From ee1c3d42d884167d1657027ca9dad17704df7231 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Tue, 20 Aug 2019 21:11:26 +0300 Subject: [PATCH 01/78] Add spinner tick judgement --- .../Judgements/OsuSpinnerTickJudgement.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 osu.Game.Rulesets.Osu/Judgements/OsuSpinnerTickJudgement.cs diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerTickJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerTickJudgement.cs new file mode 100644 index 0000000000..f9cac7a2c1 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerTickJudgement.cs @@ -0,0 +1,18 @@ +// 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.Scoring; + +namespace osu.Game.Rulesets.Osu.Judgements +{ + public class OsuSpinnerTickJudgement : OsuJudgement + { + internal bool HasBonusPoints; + + public override bool AffectsCombo => false; + + protected override int NumericResultFor(HitResult result) => 100 + (HasBonusPoints ? 1000 : 0); + + protected override double HealthIncreaseFor(HitResult result) => 0; + } +} From bb4178fa037a2b9a4d361b7a89715958d773db3e Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Tue, 20 Aug 2019 21:17:27 +0300 Subject: [PATCH 02/78] Add drawable spinner ticks implementation --- .../Objects/Drawables/DrawableSpinnerTick.cs | 49 +++++++++++++++++++ osu.Game.Rulesets.Osu/Objects/Spinner.cs | 11 +++++ osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs | 19 +++++++ .../Replays/OsuAutoGeneratorBase.cs | 2 +- 4 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs create mode 100644 osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs new file mode 100644 index 0000000000..9c316591a9 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -0,0 +1,49 @@ +// 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.Audio; +using osu.Framework.Bindables; +using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Osu.Objects.Drawables +{ + public class DrawableSpinnerTick : DrawableOsuHitObject + { + private readonly BindableDouble bonusSampleVolume = new BindableDouble(); + + private bool hasBonusPoints; + + /// + /// Whether this judgement has a bonus of 1,000 points additional to the numeric result. + /// Should be set when a spin occured after the spinner has completed. + /// + public bool HasBonusPoints + { + get => hasBonusPoints; + internal set + { + hasBonusPoints = value; + + bonusSampleVolume.Value = value ? 1 : 0; + ((OsuSpinnerTickJudgement)Result.Judgement).HasBonusPoints = value; + } + } + + public override bool DisplayResult => false; + + public DrawableSpinnerTick(SpinnerTick spinnerTick) + : base(spinnerTick) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Samples.AddAdjustment(AdjustableProperty.Volume, bonusSampleVolume); + } + + public void TriggerResult(HitResult result) => ApplyResult(r => r.Type = result); + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 8a2fd3b7aa..c32ec7be1c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -7,6 +7,8 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Osu.Replays; +using osuTK; namespace osu.Game.Rulesets.Osu.Objects { @@ -30,6 +32,15 @@ namespace osu.Game.Rulesets.Osu.Objects SpinsRequired = (int)Math.Max(1, SpinsRequired * 0.6); } + protected override void CreateNestedHitObjects() + { + base.CreateNestedHitObjects(); + + var maximumSpins = OsuAutoGeneratorBase.SPIN_RADIUS * (Duration / 1000) / MathHelper.TwoPi; + for (int i = 0; i < maximumSpins; i++) + AddNested(new SpinnerTick()); + } + public override Judgement CreateJudgement() => new OsuJudgement(); } } diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs new file mode 100644 index 0000000000..18a3dc771b --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs @@ -0,0 +1,19 @@ +// 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.Audio; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Judgements; + +namespace osu.Game.Rulesets.Osu.Objects +{ + public class SpinnerTick : OsuHitObject + { + public SpinnerTick() + { + Samples.Add(new HitSampleInfo { Name = "spinnerbonus" }); + } + + public override Judgement CreateJudgement() => new OsuSpinnerTickJudgement(); + } +} diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs index 9ab358ee12..3356a0fbe0 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Replays /// protected static readonly Vector2 SPINNER_CENTRE = OsuPlayfield.BASE_SIZE / 2; - protected const float SPIN_RADIUS = 50; + public const float SPIN_RADIUS = 50; /// /// The time in ms between each ReplayFrame. From 07795c9922cc4b3ce5197010b03fc53e0b1f565b Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Tue, 20 Aug 2019 21:50:49 +0300 Subject: [PATCH 03/78] Add logic to gain bonus score from spinner ticks --- .../Objects/Drawables/DrawableSpinner.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index a0bd301fdb..d166d6b845 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -12,7 +12,9 @@ using osu.Game.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics.Sprites; using osu.Game.Screens.Ranking; using osu.Game.Rulesets.Scoring; @@ -22,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { protected readonly Spinner Spinner; + private readonly Container ticks; + private readonly OsuSpriteText bonusCounter; + public readonly SpinnerDisc Disc; public readonly SpinnerTicks Ticks; private readonly SpinnerSpmCounter spmCounter; @@ -58,6 +63,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables InternalChildren = new Drawable[] { + ticks = new Container(), circleContainer = new Container { AutoSizeAxes = Axes.Both, @@ -115,8 +121,24 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Origin = Anchor.Centre, Y = 120, Alpha = 0 + }, + bonusCounter = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Y = -120, + Font = OsuFont.Numeric.With(size: 24), + Alpha = 0, } }; + + foreach (var tick in Spinner.NestedHitObjects.OfType()) + { + var drawableTick = new DrawableSpinnerTick(tick); + + ticks.Add(drawableTick); + AddNested(drawableTick); + } } [BackgroundDependencyLoader] @@ -182,6 +204,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.Update(); } + private int currentSpins; + protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); @@ -190,6 +214,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Ticks.Rotation = Disc.Rotation; spmCounter.SetRotation(Disc.RotationAbsolute); + var newSpins = (int)(Disc.RotationAbsolute / 360) - currentSpins; + + for (int i = currentSpins; i < currentSpins + newSpins; i++) + { + if (i < 0 || i >= ticks.Count) + break; + + var tick = ticks[i]; + + tick.HasBonusPoints = Progress >= 1 && i > Spinner.SpinsRequired; + + tick.TriggerResult(HitResult.Great); + } + + currentSpins += newSpins; + float relativeCircleScale = Spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight; Disc.ScaleTo(relativeCircleScale + (1 - relativeCircleScale) * Progress, 200, Easing.OutQuint); @@ -232,6 +272,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables break; } + if (state != ArmedState.Idle) + Schedule(() => NestedHitObjects.Where(t => !t.IsHit).OfType().ForEach(t => t.TriggerResult(HitResult.Miss))); + Expire(); } } From e4179fe4403232aa5663c80c7ee21800a20bd204 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Tue, 20 Aug 2019 21:51:32 +0300 Subject: [PATCH 04/78] Show bonus text if contains bonus points (1,000) --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index d166d6b845..b97f4e0a57 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -225,6 +225,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables tick.HasBonusPoints = Progress >= 1 && i > Spinner.SpinsRequired; + if (tick.HasBonusPoints) + bonusCounter + .TransformTextTo($"{(i - Spinner.SpinsRequired) * 1000}") + .FadeOutFromOne(1500) + .ScaleTo(1.5f).ScaleTo(1f, 1000, Easing.OutQuint); + tick.TriggerResult(HitResult.Great); } From dbf4884cbc64c736b16d334a2ed29e3f7780ce5b Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Tue, 20 Aug 2019 21:52:13 +0300 Subject: [PATCH 05/78] Adjust test spinner rotation --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs index 3ed3f3e981..6e0745d125 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Tests private class TestDrawableSpinner : DrawableSpinner { - private bool auto; + private readonly bool auto; public TestDrawableSpinner(Spinner s, bool auto) : base(s) @@ -74,12 +74,8 @@ namespace osu.Game.Rulesets.Osu.Tests protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (auto && !userTriggered && Time.Current > Spinner.StartTime + Spinner.Duration / 2 && Progress < 1) - { - // force completion only once to not break human interaction - Disc.RotationAbsolute = Spinner.SpinsRequired * 360; - auto = false; - } + if (auto && !userTriggered && Time.Current > Spinner.StartTime) + Disc.RotationAbsolute += Progress >= 1 ? 10 : (float)(Spinner.Duration / 120); base.CheckForResult(userTriggered, timeOffset); } From 6b7cb46ddaf9518e9f876535a86f385ed0db1a26 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Sat, 7 Sep 2019 17:27:02 +0300 Subject: [PATCH 06/78] Add null hit windows --- osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs index 18a3dc771b..c2104e68ee 100644 --- a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs @@ -4,6 +4,7 @@ using osu.Game.Audio; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects { @@ -15,5 +16,7 @@ namespace osu.Game.Rulesets.Osu.Objects } public override Judgement CreateJudgement() => new OsuSpinnerTickJudgement(); + + protected override HitWindows CreateHitWindows() => null; } } From 33f4a6897cd315ba7e3790378a586a9adf424b1d Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Sat, 7 Sep 2019 18:01:15 +0300 Subject: [PATCH 07/78] Assign to the text property directly --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index fc1e410d5f..62cec0f124 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -226,10 +226,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables tick.HasBonusPoints = Progress >= 1 && i > Spinner.SpinsRequired; if (tick.HasBonusPoints) + { + bonusCounter.Text = $"{(i - Spinner.SpinsRequired) * 1000}"; bonusCounter - .TransformTextTo($"{(i - Spinner.SpinsRequired) * 1000}") .FadeOutFromOne(1500) .ScaleTo(1.5f).ScaleTo(1f, 1000, Easing.OutQuint); + } tick.TriggerResult(HitResult.Great); } From 5d2fe8733997295bbbecee0cdbc947440e305d06 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Mon, 14 Oct 2019 00:38:45 +0300 Subject: [PATCH 08/78] Use empty hit windows for spinner ticks --- osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs index c2104e68ee..318e8e71a2 100644 --- a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs @@ -17,6 +17,6 @@ namespace osu.Game.Rulesets.Osu.Objects public override Judgement CreateJudgement() => new OsuSpinnerTickJudgement(); - protected override HitWindows CreateHitWindows() => null; + protected override HitWindows CreateHitWindows() => HitWindows.Empty; } } From 68e370ce7cd72c51a7eda6f9863ed37b0f86b3d5 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Mon, 14 Oct 2019 00:39:20 +0300 Subject: [PATCH 09/78] Set spinner tick start time to allow result reverting --- .../Objects/Drawables/DrawableSpinnerTick.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index 9c316591a9..21cf7b3acb 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -44,6 +44,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Samples.AddAdjustment(AdjustableProperty.Volume, bonusSampleVolume); } - public void TriggerResult(HitResult result) => ApplyResult(r => r.Type = result); + public void TriggerResult(HitResult result) + { + HitObject.StartTime = Time.Current; + ApplyResult(r => r.Type = result); + } } } From a75ae14cb20efca1673d863001736361c29c07f8 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Mon, 14 Oct 2019 00:40:36 +0300 Subject: [PATCH 10/78] Use foreach loop to avoid too long lines --- .../Objects/Drawables/DrawableSpinner.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 08e64b7ecf..965303ba7a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -12,7 +12,6 @@ using osu.Game.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Scoring; @@ -279,7 +278,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } if (state != ArmedState.Idle) - Schedule(() => NestedHitObjects.Where(t => !t.IsHit).OfType().ForEach(t => t.TriggerResult(HitResult.Miss))); + { + Schedule(() => + { + foreach (var tick in ticks.Where(t => !t.IsHit)) + tick.TriggerResult(HitResult.Miss); + }); + } } } } From a8514ecd0f220f39c214e13cd89409f1a6694c3e Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Mon, 14 Oct 2019 00:43:46 +0300 Subject: [PATCH 11/78] Add tests ensuring correct spinner ticks score results --- .../TestSceneSpinnerRotation.cs | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index cded7f0e95..b03788a7d6 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -15,6 +15,8 @@ using osu.Game.Tests.Visual; using osuTK; using System.Collections.Generic; using System.Linq; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play; using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap; namespace osu.Game.Rulesets.Osu.Tests @@ -28,6 +30,8 @@ namespace osu.Game.Rulesets.Osu.Tests protected override bool Autoplay => true; + protected override Player CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer(); + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap) { var working = new ClockBackedTestWorkingBeatmap(beatmap, new FramedClock(new ManualClock { Rate = 1 }), audioManager); @@ -69,6 +73,32 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("is rotation absolute almost same", () => Precision.AlmostEquals(drawableSpinner.Disc.RotationAbsolute, estimatedRotation, 100)); } + [Test] + public void TestSpinnerNormalBonusRewinding() + { + addSeekStep(1000); + + AddAssert("player score matching expected bonus score", () => + { + // multipled by 2 to nullify the score multiplier. (autoplay mod selected) + var totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2; + return totalScore == (int)(drawableSpinner.Disc.RotationAbsolute / 360) * 100; + }); + + addSeekStep(0); + + AddAssert("player score is 0", () => ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value == 0); + } + + [Test] + public void TestSpinnerCompleteBonusRewinding() + { + addSeekStep(2500); + addSeekStep(0); + + AddAssert("player score is 0", () => ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value == 0); + } + private void addSeekStep(double time) { AddStep($"seek to {time}", () => track.Seek(time)); @@ -85,12 +115,17 @@ namespace osu.Game.Rulesets.Osu.Tests Position = new Vector2(256, 192), EndTime = 5000, }, - // placeholder object to avoid hitting the results screen - new HitObject - { - StartTime = 99999, - } } }; + + private class ScoreExposedPlayer : TestPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + public ScoreExposedPlayer() + : base(false, false) + { + } + } } } From 10e1e512fd45abf199bea01c8d70ebfa2337df4c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 12 Dec 2019 15:15:16 +0300 Subject: [PATCH 12/78] Update the nested hitobject logic inline with new implementation --- .../Objects/Drawables/DrawableSpinner.cs | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 39330f08c3..2c21b4244a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -15,6 +15,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Ranking; @@ -131,16 +132,37 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Alpha = 0, } }; + } - foreach (var tick in Spinner.NestedHitObjects.OfType()) + protected override void AddNestedHitObject(DrawableHitObject hitObject) + { + base.AddNestedHitObject(hitObject); + + switch (hitObject) { - var drawableTick = new DrawableSpinnerTick(tick); - - ticks.Add(drawableTick); - AddNestedHitObject(drawableTick); + case DrawableSpinnerTick tick: + ticks.Add(tick); + break; } } + protected override void ClearNestedHitObjects() + { + base.ClearNestedHitObjects(); + ticks.Clear(); + } + + protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) + { + switch (hitObject) + { + case SpinnerTick tick: + return new DrawableSpinnerTick(tick); + } + + return base.CreateNestedHitObject(hitObject); + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { From 949ab4e0d3889e4ea88850b49715c1e3f8cc46d2 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 25 Dec 2019 05:34:12 +0300 Subject: [PATCH 13/78] Move spinner bonus scoring to it's own component class Also fixes counter rewinding issue and does optimizations. --- .../Objects/Drawables/DrawableSpinner.cs | 42 +-------- .../Objects/Drawables/DrawableSpinnerTick.cs | 10 ++- .../Drawables/Pieces/SpinnerBonusComponent.cs | 90 +++++++++++++++++++ 3 files changed, 100 insertions(+), 42 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index f7f4275d2a..86e8840425 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -26,11 +26,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected readonly Spinner Spinner; private readonly Container ticks; - private readonly OsuSpriteText bonusCounter; public readonly SpinnerDisc Disc; public readonly SpinnerTicks Ticks; public readonly SpinnerSpmCounter SpmCounter; + private readonly SpinnerBonusComponent bonusComponent; private readonly Container mainContainer; @@ -123,13 +123,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Y = 120, Alpha = 0 }, - bonusCounter = new OsuSpriteText + bonusComponent = new SpinnerBonusComponent(this, ticks) { Anchor = Anchor.Centre, Origin = Anchor.Centre, Y = -120, - Font = OsuFont.Numeric.With(size: 24), - Alpha = 0, } }; } @@ -226,8 +224,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.Update(); } - private int currentSpins; - protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); @@ -235,30 +231,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables circle.Rotation = Disc.Rotation; Ticks.Rotation = Disc.Rotation; SpmCounter.SetRotation(Disc.RotationAbsolute); - - var newSpins = (int)(Disc.RotationAbsolute / 360) - currentSpins; - - for (int i = currentSpins; i < currentSpins + newSpins; i++) - { - if (i < 0 || i >= ticks.Count) - break; - - var tick = ticks[i]; - - tick.HasBonusPoints = Progress >= 1 && i > Spinner.SpinsRequired; - - if (tick.HasBonusPoints) - { - bonusCounter.Text = $"{(i - Spinner.SpinsRequired) * 1000}"; - bonusCounter - .FadeOutFromOne(1500) - .ScaleTo(1.5f).ScaleTo(1f, 1000, Easing.OutQuint); - } - - tick.TriggerResult(HitResult.Great); - } - - currentSpins += newSpins; + bonusComponent.UpdateRotation(Disc.RotationAbsolute); float relativeCircleScale = Spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight; Disc.ScaleTo(relativeCircleScale + (1 - relativeCircleScale) * Progress, 200, Easing.OutQuint); @@ -299,15 +272,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables sequence.ScaleTo(Scale * 0.8f, 320, Easing.In); break; } - - if (state != ArmedState.Idle) - { - Schedule(() => - { - foreach (var tick in ticks.Where(t => !t.IsHit)) - tick.TriggerResult(HitResult.Miss); - }); - } } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index 21cf7b3acb..6512a9526e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// /// Whether this judgement has a bonus of 1,000 points additional to the numeric result. - /// Should be set when a spin occured after the spinner has completed. + /// Set when a spin occured after the spinner has completed. /// public bool HasBonusPoints { @@ -44,10 +44,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Samples.AddAdjustment(AdjustableProperty.Volume, bonusSampleVolume); } - public void TriggerResult(HitResult result) + /// + /// Apply a judgement result. + /// + /// Whether to apply a result, otherwise. + internal void TriggerResult(bool hit) { HitObject.StartTime = Time.Current; - ApplyResult(r => r.Type = result); + ApplyResult(r => r.Type = hit ? HitResult.Great : HitResult.Miss); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs new file mode 100644 index 0000000000..5c96751b3a --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs @@ -0,0 +1,90 @@ +// 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 osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +{ + /// + /// A component that tracks spinner spins and add bonus score for it. + /// + public class SpinnerBonusComponent : CompositeDrawable + { + private readonly DrawableSpinner drawableSpinner; + private readonly Container ticks; + private readonly OsuSpriteText bonusCounter; + + public SpinnerBonusComponent(DrawableSpinner drawableSpinner, Container ticks) + { + this.drawableSpinner = drawableSpinner; + this.ticks = ticks; + + drawableSpinner.OnNewResult += onNewResult; + + AutoSizeAxes = Axes.Both; + InternalChild = bonusCounter = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Numeric.With(size: 24), + Alpha = 0, + }; + } + + private int currentSpins; + + public void UpdateRotation(double rotation) + { + if (ticks.Count == 0) + return; + + int spinsRequired = ((Spinner)drawableSpinner.HitObject).SpinsRequired; + + int newSpins = Math.Clamp((int)(rotation / 360), 0, ticks.Count - 1); + int direction = Math.Sign(newSpins - currentSpins); + + while (currentSpins != newSpins) + { + var tick = ticks[currentSpins]; + + if (direction >= 0) + { + tick.HasBonusPoints = currentSpins > spinsRequired; + tick.TriggerResult(true); + } + + if (tick.HasBonusPoints) + { + bonusCounter.Text = $"{1000 * (currentSpins - spinsRequired)}"; + bonusCounter.FadeOutFromOne(1500); + bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint); + } + + currentSpins += direction; + } + } + + private void onNewResult(DrawableHitObject hitObject, JudgementResult result) + { + if (!result.HasResult || hitObject != drawableSpinner) + return; + + // Trigger a miss result for remaining ticks to avoid infinite gameplay. + foreach (var tick in ticks.Where(t => !t.IsHit)) + tick.TriggerResult(false); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + drawableSpinner.OnNewResult -= onNewResult; + } + } +} From b7565f5943f05247b6469491f052dd6287c95db3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 25 Dec 2019 05:36:58 +0300 Subject: [PATCH 14/78] Remove unnecessary using directive --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 86e8840425..edcaa947ac 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -14,7 +14,6 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Ranking; From 641ea5b950f6087d79b24b8339e2f5fa9b4bc10a Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 30 Jun 2020 13:12:33 +0200 Subject: [PATCH 15/78] Make the disabling of the win key during gameplay a toggleable setting. --- osu.Game/Configuration/OsuConfigManager.cs | 4 +++- .../Overlays/Settings/Sections/Gameplay/GeneralSettings.cs | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 9d31bc9bba..e7a86e080d 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -98,6 +98,7 @@ namespace osu.Game.Configuration Set(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised); Set(OsuSetting.IncreaseFirstObjectVisibility, true); + Set(OsuSetting.GameplayDisableWinKey, true); // Update Set(OsuSetting.ReleaseStream, ReleaseStream.Lazer); @@ -227,6 +228,7 @@ namespace osu.Game.Configuration IntroSequence, UIHoldActivationDelay, HitLighting, - MenuBackgroundSource + MenuBackgroundSource, + GameplayDisableWinKey } } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 93a02ea0e4..60197c62b5 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -76,6 +76,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay { LabelText = "Score display mode", Bindable = config.GetBindable(OsuSetting.ScoreDisplayMode) + }, + new SettingsCheckbox + { + LabelText = "Disable Win key during gameplay", + Bindable = config.GetBindable(OsuSetting.GameplayDisableWinKey) } }; } From fc1eb42a650fef5497bec37e20b5e2a29f773c07 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 1 Jul 2020 17:15:41 +0200 Subject: [PATCH 16/78] Disable windows key while in gameplay. --- osu.Desktop/OsuGameDesktop.cs | 4 + osu.Desktop/Windows/GameplayWinKeyHandler.cs | 39 ++++++++++ osu.Desktop/Windows/WindowsKey.cs | 82 ++++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 osu.Desktop/Windows/GameplayWinKeyHandler.cs create mode 100644 osu.Desktop/Windows/WindowsKey.cs diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index cd31df316a..d05a4af126 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -16,6 +16,7 @@ using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Screens.Menu; using osu.Game.Updater; +using osu.Desktop.Windows; namespace osu.Desktop { @@ -98,6 +99,9 @@ namespace osu.Desktop LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, Add); LoadComponentAsync(new DiscordRichPresence(), Add); + + if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) + LoadComponentAsync(new GameplayWinKeyHandler(), Add); } protected override void ScreenChanged(IScreen lastScreen, IScreen newScreen) diff --git a/osu.Desktop/Windows/GameplayWinKeyHandler.cs b/osu.Desktop/Windows/GameplayWinKeyHandler.cs new file mode 100644 index 0000000000..cc0150497b --- /dev/null +++ b/osu.Desktop/Windows/GameplayWinKeyHandler.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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Platform; +using osu.Game.Configuration; + +namespace osu.Desktop.Windows +{ + public class GameplayWinKeyHandler : Component + { + private Bindable winKeyEnabled; + private Bindable disableWinKey; + + private GameHost host; + + [BackgroundDependencyLoader] + private void load(GameHost host, OsuConfigManager config) + { + this.host = host; + + winKeyEnabled = host.AllowScreenSuspension.GetBoundCopy(); + winKeyEnabled.ValueChanged += toggleWinKey; + + disableWinKey = config.GetBindable(OsuSetting.GameplayDisableWinKey); + disableWinKey.BindValueChanged(t => winKeyEnabled.TriggerChange()); + } + + private void toggleWinKey(ValueChangedEvent e) + { + if (!e.NewValue && disableWinKey.Value) + host.InputThread.Scheduler.Add(WindowsKey.Disable); + else + host.InputThread.Scheduler.Add(WindowsKey.Enable); + } + } +} diff --git a/osu.Desktop/Windows/WindowsKey.cs b/osu.Desktop/Windows/WindowsKey.cs new file mode 100644 index 0000000000..748d9c55d6 --- /dev/null +++ b/osu.Desktop/Windows/WindowsKey.cs @@ -0,0 +1,82 @@ +// 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.Runtime.InteropServices; + +namespace osu.Desktop.Windows +{ + internal class WindowsKey + { + private delegate int LowLevelKeyboardProcDelegate(int nCode, int wParam, ref KdDllHookStruct lParam); + + private static bool isBlocked; + + private const int wh_keyboard_ll = 13; + private const int wm_keydown = 256; + private const int wm_syskeyup = 261; + + //Resharper disable once NotAccessedField.Local + private static LowLevelKeyboardProcDelegate keyboardHookDelegate; // keeping a reference alive for the GC + private static IntPtr keyHook; + + [StructLayout(LayoutKind.Explicit)] + private struct KdDllHookStruct + { + [FieldOffset(0)] + public readonly int VkCode; + + [FieldOffset(8)] + public readonly int Flags; + } + + private static int lowLevelKeyboardProc(int nCode, int wParam, ref KdDllHookStruct lParam) + { + if (wParam >= wm_keydown && wParam <= wm_syskeyup) + { + switch (lParam.VkCode) + { + case 0x09 when lParam.Flags == 32: // alt + tab + case 0x1b when lParam.Flags == 32: // alt + esc + case 0x5B: // left windows key + case 0x5C: // right windows key + return 1; + } + } + + return callNextHookEx(0, nCode, wParam, ref lParam); + } + + internal static void Disable() + { + if (keyHook != IntPtr.Zero || isBlocked) + return; + + keyHook = setWindowsHookEx(wh_keyboard_ll, (keyboardHookDelegate = lowLevelKeyboardProc), Marshal.GetHINSTANCE(System.Reflection.Assembly.GetExecutingAssembly().GetModules()[0]), 0); + + isBlocked = true; + } + + internal static void Enable() + { + if (keyHook == IntPtr.Zero || !isBlocked) + return; + + keyHook = unhookWindowsHookEx(keyHook); + keyboardHookDelegate = null; + + keyHook = IntPtr.Zero; + + isBlocked = false; + } + + [DllImport(@"user32.dll", EntryPoint = @"SetWindowsHookExA")] + private static extern IntPtr setWindowsHookEx(int idHook, LowLevelKeyboardProcDelegate lpfn, IntPtr hMod, int dwThreadId); + + [DllImport(@"user32.dll", EntryPoint = @"UnhookWindowsHookEx")] + private static extern IntPtr unhookWindowsHookEx(IntPtr hHook); + + [DllImport(@"user32.dll", EntryPoint = @"CallNextHookEx")] + private static extern int callNextHookEx(int hHook, int nCode, int wParam, ref KdDllHookStruct lParam); + } +} From 8869979599b2b79371b0ef2278a5f32f7200e883 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 4 Jul 2020 12:30:09 +0200 Subject: [PATCH 17/78] Trigger hook activation on bind. --- osu.Desktop/Windows/GameplayWinKeyHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/Windows/GameplayWinKeyHandler.cs b/osu.Desktop/Windows/GameplayWinKeyHandler.cs index cc0150497b..394df9dd0c 100644 --- a/osu.Desktop/Windows/GameplayWinKeyHandler.cs +++ b/osu.Desktop/Windows/GameplayWinKeyHandler.cs @@ -25,7 +25,7 @@ namespace osu.Desktop.Windows winKeyEnabled.ValueChanged += toggleWinKey; disableWinKey = config.GetBindable(OsuSetting.GameplayDisableWinKey); - disableWinKey.BindValueChanged(t => winKeyEnabled.TriggerChange()); + disableWinKey.BindValueChanged(t => winKeyEnabled.TriggerChange(), true); } private void toggleWinKey(ValueChangedEvent e) From ce5da5c51b98136503052eb11df547497019e6fb Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 5 Jul 2020 18:52:27 +0200 Subject: [PATCH 18/78] Block CTRL + ESC --- osu.Desktop/Windows/GameplayWinKeyHandler.cs | 8 ++++---- osu.Desktop/Windows/WindowsKey.cs | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Desktop/Windows/GameplayWinKeyHandler.cs b/osu.Desktop/Windows/GameplayWinKeyHandler.cs index 394df9dd0c..4f74a4f492 100644 --- a/osu.Desktop/Windows/GameplayWinKeyHandler.cs +++ b/osu.Desktop/Windows/GameplayWinKeyHandler.cs @@ -11,7 +11,7 @@ namespace osu.Desktop.Windows { public class GameplayWinKeyHandler : Component { - private Bindable winKeyEnabled; + private Bindable allowScreenSuspension; private Bindable disableWinKey; private GameHost host; @@ -21,11 +21,11 @@ namespace osu.Desktop.Windows { this.host = host; - winKeyEnabled = host.AllowScreenSuspension.GetBoundCopy(); - winKeyEnabled.ValueChanged += toggleWinKey; + allowScreenSuspension = host.AllowScreenSuspension.GetBoundCopy(); + allowScreenSuspension.ValueChanged += toggleWinKey; disableWinKey = config.GetBindable(OsuSetting.GameplayDisableWinKey); - disableWinKey.BindValueChanged(t => winKeyEnabled.TriggerChange(), true); + disableWinKey.BindValueChanged(t => allowScreenSuspension.TriggerChange(), true); } private void toggleWinKey(ValueChangedEvent e) diff --git a/osu.Desktop/Windows/WindowsKey.cs b/osu.Desktop/Windows/WindowsKey.cs index 748d9c55d6..175401aaed 100644 --- a/osu.Desktop/Windows/WindowsKey.cs +++ b/osu.Desktop/Windows/WindowsKey.cs @@ -38,6 +38,7 @@ namespace osu.Desktop.Windows { case 0x09 when lParam.Flags == 32: // alt + tab case 0x1b when lParam.Flags == 32: // alt + esc + case 0x1b when (getKeyState(0x11) & 0x8000) != 0: //ctrl + esc case 0x5B: // left windows key case 0x5C: // right windows key return 1; @@ -78,5 +79,8 @@ namespace osu.Desktop.Windows [DllImport(@"user32.dll", EntryPoint = @"CallNextHookEx")] private static extern int callNextHookEx(int hHook, int nCode, int wParam, ref KdDllHookStruct lParam); + + [DllImport(@"user32.dll", EntryPoint = @"GetKeyState")] + private static extern int getKeyState(int vkey); } } From 022e4b6335c0ebdfbc48ec4f1764ba04257a01b4 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Mon, 6 Jul 2020 11:15:56 +0200 Subject: [PATCH 19/78] Apply review suggestions. --- osu.Desktop/Windows/WindowsKey.cs | 6 ------ .../Settings/Sections/Gameplay/GeneralSettings.cs | 15 ++++++++++----- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/osu.Desktop/Windows/WindowsKey.cs b/osu.Desktop/Windows/WindowsKey.cs index 175401aaed..4a815b135e 100644 --- a/osu.Desktop/Windows/WindowsKey.cs +++ b/osu.Desktop/Windows/WindowsKey.cs @@ -36,9 +36,6 @@ namespace osu.Desktop.Windows { switch (lParam.VkCode) { - case 0x09 when lParam.Flags == 32: // alt + tab - case 0x1b when lParam.Flags == 32: // alt + esc - case 0x1b when (getKeyState(0x11) & 0x8000) != 0: //ctrl + esc case 0x5B: // left windows key case 0x5C: // right windows key return 1; @@ -79,8 +76,5 @@ namespace osu.Desktop.Windows [DllImport(@"user32.dll", EntryPoint = @"CallNextHookEx")] private static extern int callNextHookEx(int hHook, int nCode, int wParam, ref KdDllHookStruct lParam); - - [DllImport(@"user32.dll", EntryPoint = @"GetKeyState")] - private static extern int getKeyState(int vkey); } } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 60197c62b5..0149e6c3a6 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.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 osu.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Configuration; @@ -76,13 +77,17 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay { LabelText = "Score display mode", Bindable = config.GetBindable(OsuSetting.ScoreDisplayMode) - }, - new SettingsCheckbox - { - LabelText = "Disable Win key during gameplay", - Bindable = config.GetBindable(OsuSetting.GameplayDisableWinKey) } }; + + if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) + { + Add(new SettingsCheckbox + { + LabelText = "Disable Windows key during gameplay", + Bindable = config.GetBindable(OsuSetting.GameplayDisableWinKey) + }); + } } } } From 6df1b1d9ea19bfd65be8bba837c5c8b2feff38ad Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Jul 2020 20:38:33 +0900 Subject: [PATCH 20/78] Add a background beatmap difficulty manager --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 99 +++++++++++++++++++ osu.Game/OsuGameBase.cs | 1 + 2 files changed, 100 insertions(+) create mode 100644 osu.Game/Beatmaps/BeatmapDifficultyManager.cs diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs new file mode 100644 index 0000000000..f09118a24a --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -0,0 +1,99 @@ +// 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.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Framework.Threading; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Beatmaps +{ + public class BeatmapDifficultyManager : CompositeDrawable + { + // Too many simultaneous updates can lead to stutters. One thread seems to work fine for song select display purposes. + private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(1, nameof(BeatmapDifficultyManager)); + + private readonly TimedExpiryCache difficultyCache = new TimedExpiryCache { ExpiryTime = 60000 }; + private readonly BeatmapManager beatmapManager; + + public BeatmapDifficultyManager(BeatmapManager beatmapManager) + { + this.beatmapManager = beatmapManager; + } + + public Task GetDifficultyAsync([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null, + CancellationToken cancellationToken = default) + => Task.Factory.StartNew(() => GetDifficulty(beatmapInfo, rulesetInfo, mods), cancellationToken, + TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, + updateScheduler); + + public double GetDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null) + { + // Difficulty can only be computed if the beatmap is locally available. + if (beatmapInfo.ID == 0) + return 0; + + // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. + rulesetInfo ??= beatmapInfo.Ruleset; + + var key = new DifficultyCacheLookup(beatmapInfo, rulesetInfo, mods); + if (difficultyCache.TryGetValue(key, out var existing)) + return existing; + + try + { + var ruleset = rulesetInfo.CreateInstance(); + Debug.Assert(ruleset != null); + + var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(beatmapInfo)); + var attributes = calculator.Calculate(mods?.ToArray() ?? Array.Empty()); + + difficultyCache.Add(key, attributes.StarRating); + return attributes.StarRating; + } + catch + { + difficultyCache.Add(key, 0); + return 0; + } + } + + private readonly struct DifficultyCacheLookup : IEquatable + { + private readonly BeatmapInfo beatmapInfo; + private readonly RulesetInfo rulesetInfo; + private readonly IReadOnlyList mods; + + public DifficultyCacheLookup(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IEnumerable mods) + { + this.beatmapInfo = beatmapInfo; + this.rulesetInfo = rulesetInfo; + this.mods = mods?.OrderBy(m => m.Acronym).ToArray() ?? Array.Empty(); + } + + public bool Equals(DifficultyCacheLookup other) + => beatmapInfo.Equals(other.beatmapInfo) + && mods.SequenceEqual(other.mods); + + public override int GetHashCode() + { + var hashCode = new HashCode(); + + hashCode.Add(beatmapInfo.Hash); + hashCode.Add(rulesetInfo.GetHashCode()); + foreach (var mod in mods) + hashCode.Add(mod.Acronym); + + return hashCode.ToHashCode(); + } + } + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index dd120937af..1e6631ffa0 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -199,6 +199,7 @@ namespace osu.Game ScoreManager.Undelete(getBeatmapScores(item), true); }); + dependencies.Cache(new BeatmapDifficultyManager(BeatmapManager)); dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); From 3191bb506fe41950ec8f3b25be5632782499479a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Jul 2020 21:07:14 +0900 Subject: [PATCH 21/78] Improve asynchronous process --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 83 ++++++++++++------- 1 file changed, 55 insertions(+), 28 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index f09118a24a..02342e9595 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -29,32 +29,32 @@ namespace osu.Game.Beatmaps this.beatmapManager = beatmapManager; } - public Task GetDifficultyAsync([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null, - CancellationToken cancellationToken = default) - => Task.Factory.StartNew(() => GetDifficulty(beatmapInfo, rulesetInfo, mods), cancellationToken, - TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, - updateScheduler); + public async Task GetDifficultyAsync([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null, + CancellationToken cancellationToken = default) + { + if (tryGetGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) + return existing; + + return await Task.Factory.StartNew(() => getDifficulty(key), cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); + } public double GetDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null) { - // Difficulty can only be computed if the beatmap is locally available. - if (beatmapInfo.ID == 0) - return 0; - - // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. - rulesetInfo ??= beatmapInfo.Ruleset; - - var key = new DifficultyCacheLookup(beatmapInfo, rulesetInfo, mods); - if (difficultyCache.TryGetValue(key, out var existing)) + if (tryGetGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) return existing; + return getDifficulty(key); + } + + private double getDifficulty(in DifficultyCacheLookup key) + { try { - var ruleset = rulesetInfo.CreateInstance(); + var ruleset = key.RulesetInfo.CreateInstance(); Debug.Assert(ruleset != null); - var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(beatmapInfo)); - var attributes = calculator.Calculate(mods?.ToArray() ?? Array.Empty()); + var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo)); + var attributes = calculator.Calculate(key.Mods); difficultyCache.Add(key, attributes.StarRating); return attributes.StarRating; @@ -66,30 +66,57 @@ namespace osu.Game.Beatmaps } } + /// + /// Attempts to retrieve an existing difficulty for the combination. + /// + /// The . + /// The . + /// The s. + /// The existing difficulty value, if present. + /// The key that was used to perform this lookup. This can be further used to query . + /// Whether an existing difficulty was found. + private bool tryGetGetExisting(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IReadOnlyList mods, out double existingDifficulty, out DifficultyCacheLookup key) + { + // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. + rulesetInfo ??= beatmapInfo.Ruleset; + + // Difficulty can only be computed if the beatmap is locally available. + if (beatmapInfo.ID == 0) + { + existingDifficulty = 0; + key = default; + + return true; + } + + key = new DifficultyCacheLookup(beatmapInfo, rulesetInfo, mods); + return difficultyCache.TryGetValue(key, out existingDifficulty); + } + private readonly struct DifficultyCacheLookup : IEquatable { - private readonly BeatmapInfo beatmapInfo; - private readonly RulesetInfo rulesetInfo; - private readonly IReadOnlyList mods; + public readonly BeatmapInfo BeatmapInfo; + public readonly RulesetInfo RulesetInfo; + public readonly Mod[] Mods; public DifficultyCacheLookup(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IEnumerable mods) { - this.beatmapInfo = beatmapInfo; - this.rulesetInfo = rulesetInfo; - this.mods = mods?.OrderBy(m => m.Acronym).ToArray() ?? Array.Empty(); + BeatmapInfo = beatmapInfo; + RulesetInfo = rulesetInfo; + Mods = mods?.OrderBy(m => m.Acronym).ToArray() ?? Array.Empty(); } public bool Equals(DifficultyCacheLookup other) - => beatmapInfo.Equals(other.beatmapInfo) - && mods.SequenceEqual(other.mods); + => BeatmapInfo.Equals(other.BeatmapInfo) + && Mods.SequenceEqual(other.Mods); public override int GetHashCode() { var hashCode = new HashCode(); - hashCode.Add(beatmapInfo.Hash); - hashCode.Add(rulesetInfo.GetHashCode()); - foreach (var mod in mods) + hashCode.Add(BeatmapInfo.Hash); + hashCode.Add(RulesetInfo.GetHashCode()); + foreach (var mod in Mods) hashCode.Add(mod.Acronym); return hashCode.ToHashCode(); From 24f14751ce77a98c3a520e3b66c25df05666c0c6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Jul 2020 21:08:08 +0900 Subject: [PATCH 22/78] Update beatmap details SR on ruleset/mod changes --- .../Screens/Select/Details/AdvancedStats.cs | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index 02822ea608..c5fc3701f8 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -14,10 +14,13 @@ using osu.Framework.Bindables; using System.Collections.Generic; using osu.Game.Rulesets.Mods; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Overlays.Settings; +using osu.Game.Rulesets; namespace osu.Game.Screens.Select.Details { @@ -26,6 +29,12 @@ namespace osu.Game.Screens.Select.Details [Resolved] private IBindable> mods { get; set; } + [Resolved] + private IBindable ruleset { get; set; } + + [Resolved] + private BeatmapDifficultyManager difficultyManager { get; set; } + protected readonly StatisticRow FirstValue, HpDrain, Accuracy, ApproachRate; private readonly StatisticRow starDifficulty; @@ -71,6 +80,7 @@ namespace osu.Game.Screens.Select.Details { base.LoadComplete(); + ruleset.BindValueChanged(_ => updateStatistics()); mods.BindValueChanged(modsChanged, true); } @@ -132,11 +142,33 @@ namespace osu.Game.Screens.Select.Details break; } - starDifficulty.Value = ((float)(Beatmap?.StarDifficulty ?? 0), null); - HpDrain.Value = (baseDifficulty?.DrainRate ?? 0, adjustedDifficulty?.DrainRate); Accuracy.Value = (baseDifficulty?.OverallDifficulty ?? 0, adjustedDifficulty?.OverallDifficulty); ApproachRate.Value = (baseDifficulty?.ApproachRate ?? 0, adjustedDifficulty?.ApproachRate); + + updateStarDifficulty(); + } + + private CancellationTokenSource starDifficultyCancellationSource; + + private void updateStarDifficulty() + { + starDifficultyCancellationSource?.Cancel(); + + if (Beatmap == null) + return; + + var ourSource = starDifficultyCancellationSource = new CancellationTokenSource(); + + Task.WhenAll(difficultyManager.GetDifficultyAsync(Beatmap, ruleset.Value, cancellationToken: ourSource.Token), + difficultyManager.GetDifficultyAsync(Beatmap, ruleset.Value, mods.Value, ourSource.Token)).ContinueWith(t => + { + Schedule(() => + { + if (!ourSource.IsCancellationRequested) + starDifficulty.Value = ((float)t.Result[0], (float)t.Result[1]); + }); + }, ourSource.Token); } public class StatisticRow : Container, IHasAccentColour From 9a52058a7aa5a8aa153a8793c83d77b0b1d37b3f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Jul 2020 21:08:24 +0900 Subject: [PATCH 23/78] Update carousel beatmap SR on mod/ruleset changes --- .../Carousel/DrawableCarouselBeatmap.cs | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 3e4798a812..d4205a4b93 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; +using System.Threading; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -13,6 +15,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; @@ -20,6 +23,8 @@ using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osuTK; using osuTK.Graphics; @@ -41,6 +46,15 @@ namespace osu.Game.Screens.Select.Carousel [Resolved(CanBeNull = true)] private BeatmapSetOverlay beatmapOverlay { get; set; } + [Resolved] + private IBindable ruleset { get; set; } + + [Resolved] + private IBindable> mods { get; set; } + + [Resolved] + private BeatmapDifficultyManager difficultyManager { get; set; } + public DrawableCarouselBeatmap(CarouselBeatmap panel) : base(panel) { @@ -137,7 +151,6 @@ namespace osu.Game.Screens.Select.Carousel }, starCounter = new StarCounter { - Current = (float)beatmap.StarDifficulty, Scale = new Vector2(0.8f), } } @@ -147,6 +160,36 @@ namespace osu.Game.Screens.Select.Carousel } } }; + + ruleset.BindValueChanged(_ => refreshStarCounter()); + mods.BindValueChanged(_ => refreshStarCounter(), true); + } + + private ScheduledDelegate scheduledRefresh; + private CancellationTokenSource cancellationSource; + + private void refreshStarCounter() + { + scheduledRefresh?.Cancel(); + scheduledRefresh = null; + + cancellationSource?.Cancel(); + cancellationSource = null; + + // Only want to run the calculation when we become visible. + scheduledRefresh = Schedule(() => + { + var ourSource = cancellationSource = new CancellationTokenSource(); + difficultyManager.GetDifficultyAsync(beatmap, ruleset.Value, mods.Value, ourSource.Token).ContinueWith(t => + { + // We're currently on a random threadpool thread which we must exit. + Schedule(() => + { + if (!ourSource.IsCancellationRequested) + starCounter.Current = (float)t.Result; + }); + }, ourSource.Token); + }); } protected override void Selected() From 939441ae408d5f4eb7ee61dba8da54e5e056d481 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Thu, 16 Jul 2020 14:50:11 +0200 Subject: [PATCH 24/78] Disable the windows key only when in gameplay. --- osu.Desktop/Windows/GameplayWinKeyHandler.cs | 14 +++++++------- osu.Game/Configuration/SessionStatics.cs | 4 +++- osu.Game/Screens/Play/ScreenSuspensionHandler.cs | 13 ++++++++++++- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/osu.Desktop/Windows/GameplayWinKeyHandler.cs b/osu.Desktop/Windows/GameplayWinKeyHandler.cs index 4f74a4f492..d5ef89c680 100644 --- a/osu.Desktop/Windows/GameplayWinKeyHandler.cs +++ b/osu.Desktop/Windows/GameplayWinKeyHandler.cs @@ -11,26 +11,26 @@ namespace osu.Desktop.Windows { public class GameplayWinKeyHandler : Component { - private Bindable allowScreenSuspension; private Bindable disableWinKey; + private Bindable disableWinKeySetting; private GameHost host; [BackgroundDependencyLoader] - private void load(GameHost host, OsuConfigManager config) + private void load(GameHost host, OsuConfigManager config, SessionStatics statics) { this.host = host; - allowScreenSuspension = host.AllowScreenSuspension.GetBoundCopy(); - allowScreenSuspension.ValueChanged += toggleWinKey; + disableWinKey = statics.GetBindable(Static.DisableWindowsKey); + disableWinKey.ValueChanged += toggleWinKey; - disableWinKey = config.GetBindable(OsuSetting.GameplayDisableWinKey); - disableWinKey.BindValueChanged(t => allowScreenSuspension.TriggerChange(), true); + disableWinKeySetting = config.GetBindable(OsuSetting.GameplayDisableWinKey); + disableWinKeySetting.BindValueChanged(t => disableWinKey.TriggerChange(), true); } private void toggleWinKey(ValueChangedEvent e) { - if (!e.NewValue && disableWinKey.Value) + if (e.NewValue && disableWinKeySetting.Value) host.InputThread.Scheduler.Add(WindowsKey.Disable); else host.InputThread.Scheduler.Add(WindowsKey.Enable); diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index 40b2adb867..7aad79b5ad 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -12,12 +12,14 @@ namespace osu.Game.Configuration { Set(Static.LoginOverlayDisplayed, false); Set(Static.MutedAudioNotificationShownOnce, false); + Set(Static.DisableWindowsKey, false); } } public enum Static { LoginOverlayDisplayed, - MutedAudioNotificationShownOnce + MutedAudioNotificationShownOnce, + DisableWindowsKey } } diff --git a/osu.Game/Screens/Play/ScreenSuspensionHandler.cs b/osu.Game/Screens/Play/ScreenSuspensionHandler.cs index 8585a5c309..c2c7f1ac41 100644 --- a/osu.Game/Screens/Play/ScreenSuspensionHandler.cs +++ b/osu.Game/Screens/Play/ScreenSuspensionHandler.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Platform; +using osu.Game.Configuration; namespace osu.Game.Screens.Play { @@ -22,6 +23,9 @@ namespace osu.Game.Screens.Play [Resolved] private GameHost host { get; set; } + [Resolved] + private SessionStatics statics { get; set; } + public ScreenSuspensionHandler([NotNull] GameplayClockContainer gameplayClockContainer) { this.gameplayClockContainer = gameplayClockContainer ?? throw new ArgumentNullException(nameof(gameplayClockContainer)); @@ -36,7 +40,11 @@ namespace osu.Game.Screens.Play Debug.Assert(host.AllowScreenSuspension.Value); isPaused = gameplayClockContainer.IsPaused.GetBoundCopy(); - isPaused.BindValueChanged(paused => host.AllowScreenSuspension.Value = paused.NewValue, true); + isPaused.BindValueChanged(paused => + { + host.AllowScreenSuspension.Value = paused.NewValue; + statics.Set(Static.DisableWindowsKey, !paused.NewValue); + }, true); } protected override void Dispose(bool isDisposing) @@ -46,7 +54,10 @@ namespace osu.Game.Screens.Play isPaused?.UnbindAll(); if (host != null) + { host.AllowScreenSuspension.Value = true; + statics.Set(Static.DisableWindowsKey, false); + } } } } From 396ada7f39fb52a3301398c1cf8d17767da86bf6 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Thu, 16 Jul 2020 15:03:25 +0200 Subject: [PATCH 25/78] Enable windows key when a replay is loaded. --- osu.Game/Screens/Play/Player.cs | 2 +- osu.Game/Screens/Play/ScreenSuspensionHandler.cs | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 541275cf55..e0721d55f7 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -181,7 +181,7 @@ namespace osu.Game.Screens.Play InternalChild = GameplayClockContainer = new GameplayClockContainer(Beatmap.Value, Mods.Value, DrawableRuleset.GameplayStartTime); AddInternal(gameplayBeatmap = new GameplayBeatmap(playableBeatmap)); - AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); + AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer, DrawableRuleset.HasReplayLoaded)); dependencies.CacheAs(gameplayBeatmap); diff --git a/osu.Game/Screens/Play/ScreenSuspensionHandler.cs b/osu.Game/Screens/Play/ScreenSuspensionHandler.cs index c2c7f1ac41..6865db5a5e 100644 --- a/osu.Game/Screens/Play/ScreenSuspensionHandler.cs +++ b/osu.Game/Screens/Play/ScreenSuspensionHandler.cs @@ -19,6 +19,7 @@ namespace osu.Game.Screens.Play { private readonly GameplayClockContainer gameplayClockContainer; private Bindable isPaused; + private readonly Bindable hasReplayLoaded; [Resolved] private GameHost host { get; set; } @@ -26,9 +27,10 @@ namespace osu.Game.Screens.Play [Resolved] private SessionStatics statics { get; set; } - public ScreenSuspensionHandler([NotNull] GameplayClockContainer gameplayClockContainer) + public ScreenSuspensionHandler([NotNull] GameplayClockContainer gameplayClockContainer, Bindable hasReplayLoaded) { this.gameplayClockContainer = gameplayClockContainer ?? throw new ArgumentNullException(nameof(gameplayClockContainer)); + this.hasReplayLoaded = hasReplayLoaded.GetBoundCopy(); } protected override void LoadComplete() @@ -43,8 +45,9 @@ namespace osu.Game.Screens.Play isPaused.BindValueChanged(paused => { host.AllowScreenSuspension.Value = paused.NewValue; - statics.Set(Static.DisableWindowsKey, !paused.NewValue); + statics.Set(Static.DisableWindowsKey, !paused.NewValue && !hasReplayLoaded.Value); }, true); + hasReplayLoaded.BindValueChanged(_ => isPaused.TriggerChange(), true); } protected override void Dispose(bool isDisposing) @@ -52,6 +55,7 @@ namespace osu.Game.Screens.Play base.Dispose(isDisposing); isPaused?.UnbindAll(); + hasReplayLoaded.UnbindAll(); if (host != null) { From 72ace508b605dace2f6f3e684aa83fac3bae3c4c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 19 Jul 2020 11:37:10 +0900 Subject: [PATCH 26/78] Reduce memory allocations in MenuCursorContainer --- osu.Game/Graphics/Cursor/MenuCursorContainer.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs index b7ea1ba56a..02bfb3fad6 100644 --- a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs +++ b/osu.Game/Graphics/Cursor/MenuCursorContainer.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.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -55,7 +54,15 @@ namespace osu.Game.Graphics.Cursor return; } - var newTarget = inputManager.HoveredDrawables.OfType().FirstOrDefault(t => t.ProvidingUserCursor) ?? this; + IProvideCursor newTarget = this; + + foreach (var d in inputManager.HoveredDrawables) + { + if (!(d is IProvideCursor p) || !p.ProvidingUserCursor) continue; + + newTarget = p; + break; + } if (currentTarget == newTarget) return; From f48984920d5b489adba4afd4a8c3c9fedaceebe1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jul 2020 11:21:32 +0900 Subject: [PATCH 27/78] Change bonus volume logic to work --- .../Objects/Drawables/DrawableSpinnerTick.cs | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index 6512a9526e..436994e480 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.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. -using osu.Framework.Audio; -using osu.Framework.Bindables; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; @@ -10,8 +8,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSpinnerTick : DrawableOsuHitObject { - private readonly BindableDouble bonusSampleVolume = new BindableDouble(); - private bool hasBonusPoints; /// @@ -25,8 +21,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { hasBonusPoints = value; - bonusSampleVolume.Value = value ? 1 : 0; - ((OsuSpinnerTickJudgement)Result.Judgement).HasBonusPoints = value; + Samples.Volume.Value = ((OsuSpinnerTickJudgement)Result.Judgement).HasBonusPoints ? 1 : 0; } } @@ -37,13 +32,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { } - protected override void LoadComplete() - { - base.LoadComplete(); - - Samples.AddAdjustment(AdjustableProperty.Volume, bonusSampleVolume); - } - /// /// Apply a judgement result. /// From 4dd40542d519ad2f5db5a529ec864b990a0ec697 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jul 2020 11:21:58 +0900 Subject: [PATCH 28/78] Rename rotation set method to match others --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 2 +- .../Objects/Drawables/Pieces/SpinnerBonusComponent.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index b82b44f35b..2707453ab9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -227,7 +227,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Ticks.Rotation = Disc.Rotation; SpmCounter.SetRotation(Disc.CumulativeRotation); - bonusComponent.UpdateRotation(Disc.CumulativeRotation); + bonusComponent.SetRotation(Disc.CumulativeRotation); float relativeCircleScale = Spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight; float targetScale = relativeCircleScale + (1 - relativeCircleScale) * Progress; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs index 5c96751b3a..c49c10b45c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private int currentSpins; - public void UpdateRotation(double rotation) + public void SetRotation(double rotation) { if (ticks.Count == 0) return; From 35ad409da6fd60f48f66b58003058ac9d2c5b360 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 21 Jul 2020 06:59:24 +0300 Subject: [PATCH 29/78] Fix spinner bonus ticks samples not actually playing --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index 436994e480..d49766adda 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -21,7 +21,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { hasBonusPoints = value; - Samples.Volume.Value = ((OsuSpinnerTickJudgement)Result.Judgement).HasBonusPoints ? 1 : 0; + ((OsuSpinnerTickJudgement)Result.Judgement).HasBonusPoints = value; + Samples.Volume.Value = value ? 1 : 0; } } From c1442568b95548316e748e470c00c70a45bd5f9a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jul 2020 17:04:29 +0900 Subject: [PATCH 30/78] Make perfect mod ignore all non-combo-affecting hitobjects --- .../Mods/TestSceneCatchModPerfect.cs | 2 +- osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs | 6 ------ .../Skinning/TestSceneDrawableTaikoMascot.cs | 2 +- osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs | 4 ++-- .../Judgements/TaikoDrumRollJudgement.cs | 2 -- osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs | 2 -- osu.Game/Rulesets/Mods/ModPerfect.cs | 1 + 7 files changed, 5 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs index 3e06e78dba..c1b7214d72 100644 --- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods public void TestDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Droplet { StartTime = 1000 }), shouldMiss); // We only care about testing misses, hits are tested via JuiceStream - [TestCase(true)] + [TestCase(false)] public void TestTinyDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new TinyDroplet { StartTime = 1000 }), shouldMiss); } } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs b/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs index e3391c47f1..fb92399102 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs @@ -1,17 +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 osu.Game.Rulesets.Catch.Judgements; -using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Catch.Mods { public class CatchModPerfect : ModPerfect { - protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) - => !(result.Judgement is CatchBananaJudgement) - && base.FailCondition(healthProcessor, result); } } diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index d200c44a02..cb6a0decde 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Fail); - assertStateAfterResult(new JudgementResult(new DrumRoll(), new TaikoDrumRollJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Fail); + assertStateAfterResult(new JudgementResult(new DrumRoll(), new TaikoDrumRollJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Idle); } diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs index aaa634648a..0be005e1c4 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Tests }; [Test] - public void TestSpinnerDoesNotFail() + public void TestSpinnerDoesFail() { bool judged = false; AddStep("Setup judgements", () => @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Tests Player.ScoreProcessor.NewJudgement += b => judged = true; }); AddUntilStep("swell judged", () => judged); - AddAssert("not failed", () => !Player.HasFailed); + AddAssert("failed", () => Player.HasFailed); } } } diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs index 604daa929f..0d91002f4b 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs @@ -7,8 +7,6 @@ namespace osu.Game.Rulesets.Taiko.Judgements { public class TaikoDrumRollJudgement : TaikoJudgement { - public override bool AffectsCombo => false; - protected override double HealthIncreaseFor(HitResult result) { // Drum rolls can be ignored with no health penalty diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs index 29be5e0eac..4d61efd3ee 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs @@ -7,8 +7,6 @@ namespace osu.Game.Rulesets.Taiko.Judgements { public class TaikoSwellJudgement : TaikoJudgement { - public override bool AffectsCombo => false; - protected override double HealthIncreaseFor(HitResult result) { switch (result) diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs index 7fe606d584..65f1a972ed 100644 --- a/osu.Game/Rulesets/Mods/ModPerfect.cs +++ b/osu.Game/Rulesets/Mods/ModPerfect.cs @@ -17,6 +17,7 @@ namespace osu.Game.Rulesets.Mods protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) => !(result.Judgement is IgnoreJudgement) + && result.Judgement.AffectsCombo && result.Type != result.Judgement.MaxResult; } } From 05102bc1baf00b4508bf57dfe0e749569944b8ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jul 2020 18:22:37 +0900 Subject: [PATCH 31/78] Split ticks up into bonus and non-bonus --- .../Judgements/OsuSpinnerTickJudgement.cs | 18 -------------- .../Objects/Drawables/DrawableSpinner.cs | 3 +++ .../Drawables/DrawableSpinnerBonusTick.cs | 13 ++++++++++ .../Objects/Drawables/DrawableSpinnerTick.cs | 19 --------------- .../Drawables/Pieces/SpinnerBonusComponent.cs | 5 +--- osu.Game.Rulesets.Osu/Objects/Spinner.cs | 19 ++++++++++----- .../Objects/SpinnerBonusTick.cs | 24 +++++++++++++++++++ osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs | 15 +++++++----- 8 files changed, 63 insertions(+), 53 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu/Judgements/OsuSpinnerTickJudgement.cs create mode 100644 osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs create mode 100644 osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerTickJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerTickJudgement.cs deleted file mode 100644 index f9cac7a2c1..0000000000 --- a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerTickJudgement.cs +++ /dev/null @@ -1,18 +0,0 @@ -// 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.Scoring; - -namespace osu.Game.Rulesets.Osu.Judgements -{ - public class OsuSpinnerTickJudgement : OsuJudgement - { - internal bool HasBonusPoints; - - public override bool AffectsCombo => false; - - protected override int NumericResultFor(HitResult result) => 100 + (HasBonusPoints ? 1000 : 0); - - protected override double HealthIncreaseFor(HitResult result) => 0; - } -} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 2707453ab9..531d16d1d1 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -157,6 +157,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { switch (hitObject) { + case SpinnerBonusTick bonusTick: + return new DrawableSpinnerBonusTick(bonusTick); + case SpinnerTick tick: return new DrawableSpinnerTick(tick); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs new file mode 100644 index 0000000000..2e1c07c4c6 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Osu.Objects.Drawables +{ + public class DrawableSpinnerBonusTick : DrawableSpinnerTick + { + public DrawableSpinnerBonusTick(SpinnerBonusTick spinnerTick) + : base(spinnerTick) + { + } + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index d49766adda..5fb7653f5a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -1,31 +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.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSpinnerTick : DrawableOsuHitObject { - private bool hasBonusPoints; - - /// - /// Whether this judgement has a bonus of 1,000 points additional to the numeric result. - /// Set when a spin occured after the spinner has completed. - /// - public bool HasBonusPoints - { - get => hasBonusPoints; - internal set - { - hasBonusPoints = value; - - ((OsuSpinnerTickJudgement)Result.Judgement).HasBonusPoints = value; - Samples.Volume.Value = value ? 1 : 0; - } - } - public override bool DisplayResult => false; public DrawableSpinnerTick(SpinnerTick spinnerTick) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs index c49c10b45c..9a65247453 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs @@ -55,12 +55,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces var tick = ticks[currentSpins]; if (direction >= 0) - { - tick.HasBonusPoints = currentSpins > spinsRequired; tick.TriggerResult(true); - } - if (tick.HasBonusPoints) + if (tick is DrawableSpinnerBonusTick) { bonusCounter.Text = $"{1000 * (currentSpins - spinsRequired)}"; bonusCounter.FadeOutFromOne(1500); diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 4c21d9cfde..1c30058d5d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -3,13 +3,11 @@ using System; using osu.Game.Beatmaps; -using osu.Game.Rulesets.Objects.Types; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Judgements; -using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Scoring; -using osuTK; namespace osu.Game.Rulesets.Osu.Objects { @@ -28,6 +26,8 @@ namespace osu.Game.Rulesets.Osu.Objects /// public int SpinsRequired { get; protected set; } = 1; + public int MaximumBonusSpins => SpinsRequired; + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); @@ -42,9 +42,16 @@ namespace osu.Game.Rulesets.Osu.Objects { base.CreateNestedHitObjects(); - var maximumSpins = OsuAutoGeneratorBase.SPIN_RADIUS * (Duration / 1000) / MathHelper.TwoPi; - for (int i = 0; i < maximumSpins; i++) - AddNested(new SpinnerTick()); + int totalSpins = MaximumBonusSpins + SpinsRequired; + + for (int i = 0; i < totalSpins; i++) + { + double startTime = StartTime + (float)(i + 1) / totalSpins * Duration; + + AddNested(i < SpinsRequired + ? new SpinnerTick { StartTime = startTime } + : new SpinnerBonusTick { StartTime = startTime }); + } } public override Judgement CreateJudgement() => new OsuJudgement(); diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs new file mode 100644 index 0000000000..84eb58c70b --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs @@ -0,0 +1,24 @@ +// 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.Audio; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Osu.Objects +{ + public class SpinnerBonusTick : SpinnerTick + { + public SpinnerBonusTick() + { + Samples.Add(new HitSampleInfo { Name = "spinnerbonus" }); + } + + public override Judgement CreateJudgement() => new OsuSpinnerBonusTickJudgement(); + + public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement + { + protected override int NumericResultFor(HitResult result) => 1100; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs index 318e8e71a2..89ad45b267 100644 --- a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.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 osu.Game.Audio; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; @@ -10,13 +9,17 @@ namespace osu.Game.Rulesets.Osu.Objects { public class SpinnerTick : OsuHitObject { - public SpinnerTick() - { - Samples.Add(new HitSampleInfo { Name = "spinnerbonus" }); - } - public override Judgement CreateJudgement() => new OsuSpinnerTickJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; + + public class OsuSpinnerTickJudgement : OsuJudgement + { + public override bool AffectsCombo => false; + + protected override int NumericResultFor(HitResult result) => 100; + + protected override double HealthIncreaseFor(HitResult result) => 0; + } } } From 947f4e0d4c5aac6609ef6cfdf5488256402c6376 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jul 2020 19:03:17 +0900 Subject: [PATCH 32/78] Move tick handling to DrawableSpinner itself --- .../Objects/Drawables/DrawableSpinner.cs | 42 ++++++++- .../Objects/Drawables/DrawableSpinnerTick.cs | 7 +- .../Drawables/Pieces/SpinnerBonusComponent.cs | 87 ------------------- .../Drawables/Pieces/SpinnerBonusDisplay.cs | 44 ++++++++++ 4 files changed, 86 insertions(+), 94 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs create mode 100644 osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 531d16d1d1..df6eb206da 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public readonly SpinnerDisc Disc; public readonly SpinnerTicks Ticks; public readonly SpinnerSpmCounter SpmCounter; - private readonly SpinnerBonusComponent bonusComponent; + private readonly SpinnerBonusDisplay bonusDisplay; private readonly Container mainContainer; @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Y = 120, Alpha = 0 }, - bonusComponent = new SpinnerBonusComponent(this, ticks) + bonusDisplay = new SpinnerBonusDisplay { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -199,6 +199,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (userTriggered || Time.Current < Spinner.EndTime) return; + // Trigger a miss result for remaining ticks to avoid infinite gameplay. + foreach (var tick in ticks.Where(t => !t.IsHit)) + tick.TriggerResult(HitResult.Miss); + ApplyResult(r => { if (Progress >= 1) @@ -230,7 +234,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Ticks.Rotation = Disc.Rotation; SpmCounter.SetRotation(Disc.CumulativeRotation); - bonusComponent.SetRotation(Disc.CumulativeRotation); + + updateBonusScore(); float relativeCircleScale = Spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight; float targetScale = relativeCircleScale + (1 - relativeCircleScale) * Progress; @@ -239,6 +244,37 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, Disc.Rotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1)); } + private int wholeSpins; + + private void updateBonusScore() + { + if (ticks.Count == 0) + return; + + int spins = (int)(Disc.CumulativeRotation / 360); + + while (wholeSpins != spins) + { + if (wholeSpins < spins) + { + var tick = ticks.FirstOrDefault(t => !t.IsHit); + + if (tick != null) + { + tick.TriggerResult(HitResult.Great); + if (tick is DrawableSpinnerBonusTick) + bonusDisplay.SetBonusCount(spins - Spinner.SpinsRequired); + } + + wholeSpins++; + } + else + { + wholeSpins--; + } + } + } + protected override void UpdateInitialTransforms() { base.UpdateInitialTransforms(); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index 5fb7653f5a..6c9570c381 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -17,11 +17,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// /// Apply a judgement result. /// - /// Whether to apply a result, otherwise. - internal void TriggerResult(bool hit) + /// Whether to apply a result, otherwise. + internal void TriggerResult(HitResult result) { - HitObject.StartTime = Time.Current; - ApplyResult(r => r.Type = hit ? HitResult.Great : HitResult.Miss); + ApplyResult(r => r.Type = result); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs deleted file mode 100644 index 9a65247453..0000000000 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs +++ /dev/null @@ -1,87 +0,0 @@ -// 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 osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects.Drawables; - -namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces -{ - /// - /// A component that tracks spinner spins and add bonus score for it. - /// - public class SpinnerBonusComponent : CompositeDrawable - { - private readonly DrawableSpinner drawableSpinner; - private readonly Container ticks; - private readonly OsuSpriteText bonusCounter; - - public SpinnerBonusComponent(DrawableSpinner drawableSpinner, Container ticks) - { - this.drawableSpinner = drawableSpinner; - this.ticks = ticks; - - drawableSpinner.OnNewResult += onNewResult; - - AutoSizeAxes = Axes.Both; - InternalChild = bonusCounter = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.Numeric.With(size: 24), - Alpha = 0, - }; - } - - private int currentSpins; - - public void SetRotation(double rotation) - { - if (ticks.Count == 0) - return; - - int spinsRequired = ((Spinner)drawableSpinner.HitObject).SpinsRequired; - - int newSpins = Math.Clamp((int)(rotation / 360), 0, ticks.Count - 1); - int direction = Math.Sign(newSpins - currentSpins); - - while (currentSpins != newSpins) - { - var tick = ticks[currentSpins]; - - if (direction >= 0) - tick.TriggerResult(true); - - if (tick is DrawableSpinnerBonusTick) - { - bonusCounter.Text = $"{1000 * (currentSpins - spinsRequired)}"; - bonusCounter.FadeOutFromOne(1500); - bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint); - } - - currentSpins += direction; - } - } - - private void onNewResult(DrawableHitObject hitObject, JudgementResult result) - { - if (!result.HasResult || hitObject != drawableSpinner) - return; - - // Trigger a miss result for remaining ticks to avoid infinite gameplay. - foreach (var tick in ticks.Where(t => !t.IsHit)) - tick.TriggerResult(false); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - drawableSpinner.OnNewResult -= onNewResult; - } - } -} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs new file mode 100644 index 0000000000..76d7f1843e --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs @@ -0,0 +1,44 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +{ + /// + /// A component that tracks spinner spins and add bonus score for it. + /// + public class SpinnerBonusDisplay : CompositeDrawable + { + private readonly OsuSpriteText bonusCounter; + + public SpinnerBonusDisplay() + { + AutoSizeAxes = Axes.Both; + + InternalChild = bonusCounter = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Numeric.With(size: 24), + Alpha = 0, + }; + } + + private int displayedCount; + + public void SetBonusCount(int count) + { + if (displayedCount == count) + return; + + displayedCount = count; + bonusCounter.Text = $"{1000 * count}"; + bonusCounter.FadeOutFromOne(1500); + bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint); + } + } +} From 7f2ae694cc96e41175932d16353fa3e1c0a3e9ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jul 2020 19:21:30 +0900 Subject: [PATCH 33/78] Simplify rewind handling --- .../Objects/Drawables/DrawableSpinner.cs | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index df6eb206da..a8ecb60038 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -253,25 +253,26 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables int spins = (int)(Disc.CumulativeRotation / 360); + if (spins < wholeSpins) + { + // rewinding, silently handle + wholeSpins = spins; + return; + } + while (wholeSpins != spins) { - if (wholeSpins < spins) - { - var tick = ticks.FirstOrDefault(t => !t.IsHit); + var tick = ticks.FirstOrDefault(t => !t.IsHit); - if (tick != null) - { - tick.TriggerResult(HitResult.Great); - if (tick is DrawableSpinnerBonusTick) - bonusDisplay.SetBonusCount(spins - Spinner.SpinsRequired); - } - - wholeSpins++; - } - else + // tick may be null if we've hit the spin limit. + if (tick != null) { - wholeSpins--; + tick.TriggerResult(HitResult.Great); + if (tick is DrawableSpinnerBonusTick) + bonusDisplay.SetBonusCount(spins - Spinner.SpinsRequired); } + + wholeSpins++; } } From a4680d7a8945ded3804b0a0b84500a0a47241e44 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jul 2020 19:22:42 +0900 Subject: [PATCH 34/78] Reduce test range as to not hit spin cat --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 0f1cbcd44c..6e277ff37e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -164,13 +164,13 @@ namespace osu.Game.Rulesets.Osu.Tests { double estimatedSpm = 0; - addSeekStep(2500); + addSeekStep(1000); AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute); - addSeekStep(5000); + addSeekStep(2000); AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0)); - addSeekStep(2500); + addSeekStep(1000); AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0)); } From 1560e1786a09475d4537bfc02b881d8bb2f422f3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jul 2020 19:48:44 +0900 Subject: [PATCH 35/78] Revert back to bool for application --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 4 ++-- .../Objects/Drawables/DrawableSpinnerTick.cs | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index a8ecb60038..ecf78efdd9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -201,7 +201,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // Trigger a miss result for remaining ticks to avoid infinite gameplay. foreach (var tick in ticks.Where(t => !t.IsHit)) - tick.TriggerResult(HitResult.Miss); + tick.TriggerResult(false); ApplyResult(r => { @@ -267,7 +267,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // tick may be null if we've hit the spin limit. if (tick != null) { - tick.TriggerResult(HitResult.Great); + tick.TriggerResult(true); if (tick is DrawableSpinnerBonusTick) bonusDisplay.SetBonusCount(spins - Spinner.SpinsRequired); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index 6c9570c381..c390b673be 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -17,10 +17,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// /// Apply a judgement result. /// - /// Whether to apply a result, otherwise. - internal void TriggerResult(HitResult result) - { - ApplyResult(r => r.Type = result); - } + /// Whether this tick was reached. + internal void TriggerResult(bool hit) => ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : HitResult.Miss); } } From bc079fccf52d5d338609ca87249208a899343958 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jul 2020 19:52:16 +0900 Subject: [PATCH 36/78] Add health drain for spinner ticks --- osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs | 2 ++ osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs index 84eb58c70b..6ca2d4d72d 100644 --- a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs @@ -19,6 +19,8 @@ namespace osu.Game.Rulesets.Osu.Objects public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement { protected override int NumericResultFor(HitResult result) => 1100; + + protected override double HealthIncreaseFor(HitResult result) => base.HealthIncreaseFor(result) * 2; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs index 89ad45b267..c81348fbbf 100644 --- a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Objects protected override int NumericResultFor(HitResult result) => 100; - protected override double HealthIncreaseFor(HitResult result) => 0; + protected override double HealthIncreaseFor(HitResult result) => result == MaxResult ? 0.6 * base.HealthIncreaseFor(result) : 0; } } } From 107b5ca4f2ab5ca29356aad95a852cb28aa4e856 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Jul 2020 23:13:04 +0900 Subject: [PATCH 37/78] Add support for bindable retrieval --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 186 ++++++++++++++++-- osu.Game/OsuGameBase.cs | 5 +- .../Carousel/DrawableCarouselBeatmap.cs | 59 ++---- .../Screens/Select/Details/AdvancedStats.cs | 25 ++- 4 files changed, 208 insertions(+), 67 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index 02342e9595..379cb6aa63 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -9,7 +9,9 @@ using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; +using osu.Framework.Lists; using osu.Framework.Threading; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -21,32 +23,154 @@ namespace osu.Game.Beatmaps // Too many simultaneous updates can lead to stutters. One thread seems to work fine for song select display purposes. private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(1, nameof(BeatmapDifficultyManager)); - private readonly TimedExpiryCache difficultyCache = new TimedExpiryCache { ExpiryTime = 60000 }; - private readonly BeatmapManager beatmapManager; + // A cache that keeps references to BeatmapInfos for 60sec. + private readonly TimedExpiryCache difficultyCache = new TimedExpiryCache { ExpiryTime = 60000 }; - public BeatmapDifficultyManager(BeatmapManager beatmapManager) + // All bindables that should be updated along with the current ruleset + mods. + private readonly LockedWeakList trackedBindables = new LockedWeakList(); + + [Resolved] + private BeatmapManager beatmapManager { get; set; } + + [Resolved] + private Bindable currentRuleset { get; set; } + + [Resolved] + private Bindable> currentMods { get; set; } + + protected override void LoadComplete() { - this.beatmapManager = beatmapManager; + base.LoadComplete(); + + currentRuleset.BindValueChanged(_ => updateTrackedBindables()); + currentMods.BindValueChanged(_ => updateTrackedBindables(), true); } - public async Task GetDifficultyAsync([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null, - CancellationToken cancellationToken = default) + /// + /// Retrieves an containing the star difficulty of a with a given and combination. + /// + /// + /// This will not update to follow the currently-selected ruleset and mods. + /// + /// The to get the difficulty of. + /// The to get the difficulty with. + /// The s to get the difficulty with. + /// An optional which stops updating the star difficulty for the given . + /// An that is updated to contain the star difficulty when it becomes available. + public IBindable GetUntrackedBindable([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null, + CancellationToken cancellationToken = default) + => createBindable(beatmapInfo, rulesetInfo, mods, cancellationToken); + + /// + /// Retrieves a containing the star difficulty of a that follows the user's currently-selected ruleset and mods. + /// + /// + /// Ensure to hold a local reference of the returned in order to receive value-changed events. + /// + /// The to get the difficulty of. + /// An optional which stops updating the star difficulty for the given . + /// An that is updated to contain the star difficulty when it becomes available, or when the currently-selected ruleset and mods change. + public IBindable GetTrackedBindable([NotNull] BeatmapInfo beatmapInfo, CancellationToken cancellationToken = default) + { + var bindable = createBindable(beatmapInfo, currentRuleset.Value, currentMods.Value, cancellationToken); + trackedBindables.Add(bindable); + return bindable; + } + + /// + /// Retrieves the difficulty of a . + /// + /// The to get the difficulty of. + /// The to get the difficulty with. + /// The s to get the difficulty with. + /// An optional which stops computing the star difficulty. + /// The . + public async Task GetDifficultyAsync([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null, + CancellationToken cancellationToken = default) { if (tryGetGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) return existing; - return await Task.Factory.StartNew(() => getDifficulty(key), cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); + return await Task.Factory.StartNew(() => computeDifficulty(key), cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); } - public double GetDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null) + /// + /// Retrieves the difficulty of a . + /// + /// The to get the difficulty of. + /// The to get the difficulty with. + /// The s to get the difficulty with. + /// The . + public StarDifficulty GetDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null) { if (tryGetGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) return existing; - return getDifficulty(key); + return computeDifficulty(key); } - private double getDifficulty(in DifficultyCacheLookup key) + private CancellationTokenSource trackedUpdateCancellationSource; + + /// + /// Updates all tracked using the current ruleset and mods. + /// + private void updateTrackedBindables() + { + trackedUpdateCancellationSource?.Cancel(); + trackedUpdateCancellationSource = new CancellationTokenSource(); + + foreach (var b in trackedBindables) + { + if (trackedUpdateCancellationSource.IsCancellationRequested) + break; + + using (var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(trackedUpdateCancellationSource.Token, b.CancellationToken)) + updateBindable(b, currentRuleset.Value, currentMods.Value, linkedSource.Token); + } + } + + /// + /// Updates the value of a with a given ruleset + mods. + /// + /// The to update. + /// The to update with. + /// The s to update with. + /// A token that may be used to cancel this update. + private void updateBindable([NotNull] BindableStarDifficulty bindable, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IReadOnlyList mods, CancellationToken cancellationToken = default) + { + GetDifficultyAsync(bindable.Beatmap, rulesetInfo, mods, cancellationToken).ContinueWith(t => + { + // We're on a threadpool thread, but we should exit back to the update thread so consumers can safely handle value-changed events. + Schedule(() => + { + if (!cancellationToken.IsCancellationRequested) + bindable.Value = t.Result; + }); + }, cancellationToken); + } + + /// + /// Creates a new and triggers an initial value update. + /// + /// The that star difficulty should correspond to. + /// The initial to get the difficulty with. + /// The initial s to get the difficulty with. + /// An optional which stops updating the star difficulty for the given . + /// The . + private BindableStarDifficulty createBindable([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo initialRulesetInfo, [CanBeNull] IReadOnlyList initialMods, + CancellationToken cancellationToken) + { + var bindable = new BindableStarDifficulty(beatmapInfo, cancellationToken); + updateBindable(bindable, initialRulesetInfo, initialMods, cancellationToken); + return bindable; + } + + /// + /// Computes the difficulty defined by a key, and stores it to the timed cache. + /// + /// The that defines the computation parameters. + /// The . + private StarDifficulty computeDifficulty(in DifficultyCacheLookup key) { try { @@ -56,13 +180,17 @@ namespace osu.Game.Beatmaps var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo)); var attributes = calculator.Calculate(key.Mods); - difficultyCache.Add(key, attributes.StarRating); - return attributes.StarRating; + var difficulty = new StarDifficulty(attributes.StarRating); + difficultyCache.Add(key, difficulty); + + return difficulty; } catch { - difficultyCache.Add(key, 0); - return 0; + var difficulty = new StarDifficulty(0); + difficultyCache.Add(key, difficulty); + + return difficulty; } } @@ -73,9 +201,9 @@ namespace osu.Game.Beatmaps /// The . /// The s. /// The existing difficulty value, if present. - /// The key that was used to perform this lookup. This can be further used to query . + /// The key that was used to perform this lookup. This can be further used to query . /// Whether an existing difficulty was found. - private bool tryGetGetExisting(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IReadOnlyList mods, out double existingDifficulty, out DifficultyCacheLookup key) + private bool tryGetGetExisting(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IReadOnlyList mods, out StarDifficulty existingDifficulty, out DifficultyCacheLookup key) { // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. rulesetInfo ??= beatmapInfo.Ruleset; @@ -83,7 +211,7 @@ namespace osu.Game.Beatmaps // Difficulty can only be computed if the beatmap is locally available. if (beatmapInfo.ID == 0) { - existingDifficulty = 0; + existingDifficulty = new StarDifficulty(0); key = default; return true; @@ -122,5 +250,29 @@ namespace osu.Game.Beatmaps return hashCode.ToHashCode(); } } + + private class BindableStarDifficulty : Bindable + { + public readonly BeatmapInfo Beatmap; + public readonly CancellationToken CancellationToken; + + public BindableStarDifficulty(BeatmapInfo beatmap, CancellationToken cancellationToken) + { + Beatmap = beatmap; + CancellationToken = cancellationToken; + } + } + } + + public readonly struct StarDifficulty + { + public readonly double Stars; + + public StarDifficulty(double stars) + { + Stars = stars; + + // Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...) + } } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 1e6631ffa0..fe5c0704b7 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -199,7 +199,10 @@ namespace osu.Game ScoreManager.Undelete(getBeatmapScores(item), true); }); - dependencies.Cache(new BeatmapDifficultyManager(BeatmapManager)); + var difficultyManager = new BeatmapDifficultyManager(); + dependencies.Cache(difficultyManager); + AddInternal(difficultyManager); + dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index d4205a4b93..d5aeecae04 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -15,7 +15,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; -using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; @@ -23,8 +22,6 @@ using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; using osuTK; using osuTK.Graphics; @@ -46,15 +43,12 @@ namespace osu.Game.Screens.Select.Carousel [Resolved(CanBeNull = true)] private BeatmapSetOverlay beatmapOverlay { get; set; } - [Resolved] - private IBindable ruleset { get; set; } - - [Resolved] - private IBindable> mods { get; set; } - [Resolved] private BeatmapDifficultyManager difficultyManager { get; set; } + private IBindable starDifficultyBindable; + private CancellationTokenSource starDifficultyCancellationSource; + public DrawableCarouselBeatmap(CarouselBeatmap panel) : base(panel) { @@ -160,36 +154,6 @@ namespace osu.Game.Screens.Select.Carousel } } }; - - ruleset.BindValueChanged(_ => refreshStarCounter()); - mods.BindValueChanged(_ => refreshStarCounter(), true); - } - - private ScheduledDelegate scheduledRefresh; - private CancellationTokenSource cancellationSource; - - private void refreshStarCounter() - { - scheduledRefresh?.Cancel(); - scheduledRefresh = null; - - cancellationSource?.Cancel(); - cancellationSource = null; - - // Only want to run the calculation when we become visible. - scheduledRefresh = Schedule(() => - { - var ourSource = cancellationSource = new CancellationTokenSource(); - difficultyManager.GetDifficultyAsync(beatmap, ruleset.Value, mods.Value, ourSource.Token).ContinueWith(t => - { - // We're currently on a random threadpool thread which we must exit. - Schedule(() => - { - if (!ourSource.IsCancellationRequested) - starCounter.Current = (float)t.Result; - }); - }, ourSource.Token); - }); } protected override void Selected() @@ -224,6 +188,17 @@ namespace osu.Game.Screens.Select.Carousel if (Item.State.Value != CarouselItemState.Collapsed && Alpha == 0) starCounter.ReplayAnimation(); + if (Item.State.Value == CarouselItemState.Collapsed) + starDifficultyCancellationSource?.Cancel(); + else + { + starDifficultyCancellationSource?.Cancel(); + + // We've potentially cancelled the computation above so a new bindable is required. + starDifficultyBindable = difficultyManager.GetTrackedBindable(beatmap, (starDifficultyCancellationSource = new CancellationTokenSource()).Token); + starDifficultyBindable.BindValueChanged(d => starCounter.Current = (float)d.NewValue.Stars, true); + } + base.ApplyState(); } @@ -248,5 +223,11 @@ namespace osu.Game.Screens.Select.Carousel return items.ToArray(); } } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + starDifficultyCancellationSource?.Cancel(); + } } } diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index c5fc3701f8..aefba397b9 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -15,7 +15,6 @@ using System.Collections.Generic; using osu.Game.Rulesets.Mods; using System.Linq; using System.Threading; -using System.Threading.Tasks; using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Configuration; @@ -149,6 +148,8 @@ namespace osu.Game.Screens.Select.Details updateStarDifficulty(); } + private IBindable normalStarDifficulty; + private IBindable moddedStarDifficulty; private CancellationTokenSource starDifficultyCancellationSource; private void updateStarDifficulty() @@ -160,15 +161,19 @@ namespace osu.Game.Screens.Select.Details var ourSource = starDifficultyCancellationSource = new CancellationTokenSource(); - Task.WhenAll(difficultyManager.GetDifficultyAsync(Beatmap, ruleset.Value, cancellationToken: ourSource.Token), - difficultyManager.GetDifficultyAsync(Beatmap, ruleset.Value, mods.Value, ourSource.Token)).ContinueWith(t => - { - Schedule(() => - { - if (!ourSource.IsCancellationRequested) - starDifficulty.Value = ((float)t.Result[0], (float)t.Result[1]); - }); - }, ourSource.Token); + normalStarDifficulty = difficultyManager.GetUntrackedBindable(Beatmap, ruleset.Value, cancellationToken: ourSource.Token); + moddedStarDifficulty = difficultyManager.GetUntrackedBindable(Beatmap, ruleset.Value, mods.Value, ourSource.Token); + + normalStarDifficulty.BindValueChanged(_ => updateDisplay()); + moddedStarDifficulty.BindValueChanged(_ => updateDisplay(), true); + + void updateDisplay() => starDifficulty.Value = ((float)normalStarDifficulty.Value.Stars, (float)moddedStarDifficulty.Value.Stars); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + starDifficultyCancellationSource?.Cancel(); } public class StatisticRow : Container, IHasAccentColour From 00e6217f60c9d1981e1eb16e1b21b39a70b844a9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Jul 2020 23:50:54 +0900 Subject: [PATCH 38/78] Don't store BeatmapInfo/RulesetInfo references, remove TimedExpiryCache --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 49 +++++++++---------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index 379cb6aa63..b469ca78fb 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -24,7 +25,7 @@ namespace osu.Game.Beatmaps private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(1, nameof(BeatmapDifficultyManager)); // A cache that keeps references to BeatmapInfos for 60sec. - private readonly TimedExpiryCache difficultyCache = new TimedExpiryCache { ExpiryTime = 60000 }; + private readonly ConcurrentDictionary difficultyCache = new ConcurrentDictionary(); // All bindables that should be updated along with the current ruleset + mods. private readonly LockedWeakList trackedBindables = new LockedWeakList(); @@ -91,7 +92,8 @@ namespace osu.Game.Beatmaps if (tryGetGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) return existing; - return await Task.Factory.StartNew(() => computeDifficulty(key), cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); + return await Task.Factory.StartNew(() => computeDifficulty(key, beatmapInfo, rulesetInfo), cancellationToken, + TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); } /// @@ -106,7 +108,7 @@ namespace osu.Game.Beatmaps if (tryGetGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) return existing; - return computeDifficulty(key); + return computeDifficulty(key, beatmapInfo, rulesetInfo); } private CancellationTokenSource trackedUpdateCancellationSource; @@ -169,28 +171,24 @@ namespace osu.Game.Beatmaps /// Computes the difficulty defined by a key, and stores it to the timed cache. /// /// The that defines the computation parameters. + /// The to compute the difficulty of. + /// The to compute the difficulty with. /// The . - private StarDifficulty computeDifficulty(in DifficultyCacheLookup key) + private StarDifficulty computeDifficulty(in DifficultyCacheLookup key, BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo) { try { - var ruleset = key.RulesetInfo.CreateInstance(); + var ruleset = rulesetInfo.CreateInstance(); Debug.Assert(ruleset != null); - var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo)); + var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(beatmapInfo)); var attributes = calculator.Calculate(key.Mods); - var difficulty = new StarDifficulty(attributes.StarRating); - difficultyCache.Add(key, difficulty); - - return difficulty; + return difficultyCache[key] = new StarDifficulty(attributes.StarRating); } catch { - var difficulty = new StarDifficulty(0); - difficultyCache.Add(key, difficulty); - - return difficulty; + return difficultyCache[key] = new StarDifficulty(0); } } @@ -208,8 +206,8 @@ namespace osu.Game.Beatmaps // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. rulesetInfo ??= beatmapInfo.Ruleset; - // Difficulty can only be computed if the beatmap is locally available. - if (beatmapInfo.ID == 0) + // Difficulty can only be computed if the beatmap and ruleset are locally available. + if (beatmapInfo.ID == 0 || rulesetInfo.ID == null) { existingDifficulty = new StarDifficulty(0); key = default; @@ -217,33 +215,34 @@ namespace osu.Game.Beatmaps return true; } - key = new DifficultyCacheLookup(beatmapInfo, rulesetInfo, mods); + key = new DifficultyCacheLookup(beatmapInfo.ID, rulesetInfo.ID.Value, mods); return difficultyCache.TryGetValue(key, out existingDifficulty); } private readonly struct DifficultyCacheLookup : IEquatable { - public readonly BeatmapInfo BeatmapInfo; - public readonly RulesetInfo RulesetInfo; + public readonly int BeatmapId; + public readonly int RulesetId; public readonly Mod[] Mods; - public DifficultyCacheLookup(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IEnumerable mods) + public DifficultyCacheLookup(int beatmapId, int rulesetId, IEnumerable mods) { - BeatmapInfo = beatmapInfo; - RulesetInfo = rulesetInfo; + BeatmapId = beatmapId; + RulesetId = rulesetId; Mods = mods?.OrderBy(m => m.Acronym).ToArray() ?? Array.Empty(); } public bool Equals(DifficultyCacheLookup other) - => BeatmapInfo.Equals(other.BeatmapInfo) + => BeatmapId == other.BeatmapId + && RulesetId == other.RulesetId && Mods.SequenceEqual(other.Mods); public override int GetHashCode() { var hashCode = new HashCode(); - hashCode.Add(BeatmapInfo.Hash); - hashCode.Add(RulesetInfo.GetHashCode()); + hashCode.Add(BeatmapId); + hashCode.Add(RulesetId); foreach (var mod in Mods) hashCode.Add(mod.Acronym); From e96f8f1cb652e4d312758b20414f74c01d144ca0 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 21 Jul 2020 20:02:22 +0300 Subject: [PATCH 39/78] Make content side padding adjustable for OverlayHeader --- .../UserInterface/TestSceneOverlayHeader.cs | 20 +++++++++++--- osu.Game/Overlays/OverlayHeader.cs | 27 ++++++++++++++----- osu.Game/Overlays/TabControlOverlayHeader.cs | 24 ++++++++++++++--- 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs index 60af5b37ef..01c13dbc97 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs @@ -36,11 +36,11 @@ namespace osu.Game.Tests.Visual.UserInterface } }); - addHeader("Orange OverlayHeader (no background)", new TestNoBackgroundHeader(), OverlayColourScheme.Orange); - addHeader("Blue OverlayHeader", new TestNoControlHeader(), OverlayColourScheme.Blue); + addHeader("Orange OverlayHeader (no background, 100 padding)", new TestNoBackgroundHeader(), OverlayColourScheme.Orange); + addHeader("Blue OverlayHeader (default 70 padding)", new TestNoControlHeader(), OverlayColourScheme.Blue); addHeader("Green TabControlOverlayHeader (string) with ruleset selector", new TestStringTabControlHeader(), OverlayColourScheme.Green); - addHeader("Pink TabControlOverlayHeader (enum)", new TestEnumTabControlHeader(), OverlayColourScheme.Pink); - addHeader("Red BreadcrumbControlOverlayHeader (no background)", new TestBreadcrumbControlHeader(), OverlayColourScheme.Red); + addHeader("Pink TabControlOverlayHeader (enum, 30 padding)", new TestEnumTabControlHeader(), OverlayColourScheme.Pink); + addHeader("Red BreadcrumbControlOverlayHeader (no background, 10 padding)", new TestBreadcrumbControlHeader(), OverlayColourScheme.Red); } private void addHeader(string name, OverlayHeader header, OverlayColourScheme colourScheme) @@ -86,6 +86,11 @@ namespace osu.Game.Tests.Visual.UserInterface private class TestNoBackgroundHeader : OverlayHeader { protected override OverlayTitle CreateTitle() => new TestTitle(); + + public TestNoBackgroundHeader() + { + ContentSidePadding = 100; + } } private class TestNoControlHeader : OverlayHeader @@ -112,6 +117,11 @@ namespace osu.Game.Tests.Visual.UserInterface private class TestEnumTabControlHeader : TabControlOverlayHeader { + public TestEnumTabControlHeader() + { + ContentSidePadding = 30; + } + protected override Drawable CreateBackground() => new OverlayHeaderBackground(@"Headers/rankings"); protected override OverlayTitle CreateTitle() => new TestTitle(); @@ -130,6 +140,8 @@ namespace osu.Game.Tests.Visual.UserInterface public TestBreadcrumbControlHeader() { + ContentSidePadding = 10; + TabControl.AddItem("tab1"); TabControl.AddItem("tab2"); TabControl.Current.Value = "tab2"; diff --git a/osu.Game/Overlays/OverlayHeader.cs b/osu.Game/Overlays/OverlayHeader.cs index dbc934bde9..c9b9e3b836 100644 --- a/osu.Game/Overlays/OverlayHeader.cs +++ b/osu.Game/Overlays/OverlayHeader.cs @@ -12,9 +12,26 @@ namespace osu.Game.Overlays { public abstract class OverlayHeader : Container { - public const int CONTENT_X_MARGIN = 50; + private float contentSidePadding; + + /// + /// Horizontal padding of the header content. + /// + protected float ContentSidePadding + { + get => contentSidePadding; + set + { + contentSidePadding = value; + content.Padding = new MarginPadding + { + Horizontal = value + }; + } + } private readonly Box titleBackground; + private readonly Container content; protected readonly FillFlowContainer HeaderInfo; @@ -50,14 +67,10 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both, Colour = Color4.Gray, }, - new Container + content = new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding - { - Horizontal = CONTENT_X_MARGIN, - }, Children = new[] { CreateTitle().With(title => @@ -79,6 +92,8 @@ namespace osu.Game.Overlays CreateContent() } }); + + ContentSidePadding = 70; } [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/TabControlOverlayHeader.cs b/osu.Game/Overlays/TabControlOverlayHeader.cs index e8e000f441..61605d9e9e 100644 --- a/osu.Game/Overlays/TabControlOverlayHeader.cs +++ b/osu.Game/Overlays/TabControlOverlayHeader.cs @@ -22,6 +22,7 @@ namespace osu.Game.Overlays protected OsuTabControl TabControl; private readonly Box controlBackground; + private readonly Container tabControlContainer; private readonly BindableWithCurrent current = new BindableWithCurrent(); public Bindable Current @@ -30,6 +31,16 @@ namespace osu.Game.Overlays set => current.Current = value; } + protected new float ContentSidePadding + { + get => base.ContentSidePadding; + set + { + base.ContentSidePadding = value; + tabControlContainer.Padding = new MarginPadding { Horizontal = value }; + } + } + protected TabControlOverlayHeader() { HeaderInfo.Add(new Container @@ -42,11 +53,16 @@ namespace osu.Game.Overlays { RelativeSizeAxes = Axes.Both, }, - TabControl = CreateTabControl().With(control => + tabControlContainer = new Container { - control.Margin = new MarginPadding { Left = CONTENT_X_MARGIN }; - control.Current = Current; - }) + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = ContentSidePadding }, + Child = TabControl = CreateTabControl().With(control => + { + control.Current = Current; + }) + } } }); } From 0145ca09e5d8ebd98a85857c6fab121d7c112143 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 21 Jul 2020 20:11:10 +0300 Subject: [PATCH 40/78] Apply changes to overlays --- osu.Game/Overlays/OverlayHeader.cs | 2 +- osu.Game/Overlays/Profile/ProfileHeader.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/OverlayHeader.cs b/osu.Game/Overlays/OverlayHeader.cs index c9b9e3b836..cc7f798c4a 100644 --- a/osu.Game/Overlays/OverlayHeader.cs +++ b/osu.Game/Overlays/OverlayHeader.cs @@ -93,7 +93,7 @@ namespace osu.Game.Overlays } }); - ContentSidePadding = 70; + ContentSidePadding = 50; } [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index 0161d91daa..2895fa0726 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -23,6 +23,8 @@ namespace osu.Game.Overlays.Profile public ProfileHeader() { + ContentSidePadding = 70; + User.ValueChanged += e => updateDisplay(e.NewValue); TabControl.AddItem("info"); From ad9492804a645ea851f815b23878e4ab98211f6c Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 21 Jul 2020 22:56:44 +0300 Subject: [PATCH 41/78] Apply suggestions --- osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs | 2 +- osu.Game/Overlays/Profile/ProfileHeader.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs index 01c13dbc97..2a76b8e265 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.UserInterface }); addHeader("Orange OverlayHeader (no background, 100 padding)", new TestNoBackgroundHeader(), OverlayColourScheme.Orange); - addHeader("Blue OverlayHeader (default 70 padding)", new TestNoControlHeader(), OverlayColourScheme.Blue); + addHeader("Blue OverlayHeader (default 50 padding)", new TestNoControlHeader(), OverlayColourScheme.Blue); addHeader("Green TabControlOverlayHeader (string) with ruleset selector", new TestStringTabControlHeader(), OverlayColourScheme.Green); addHeader("Pink TabControlOverlayHeader (enum, 30 padding)", new TestEnumTabControlHeader(), OverlayColourScheme.Pink); addHeader("Red BreadcrumbControlOverlayHeader (no background, 10 padding)", new TestBreadcrumbControlHeader(), OverlayColourScheme.Red); diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index 2895fa0726..2e5f1071f2 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -23,7 +23,7 @@ namespace osu.Game.Overlays.Profile public ProfileHeader() { - ContentSidePadding = 70; + ContentSidePadding = UserProfileOverlay.CONTENT_X_MARGIN; User.ValueChanged += e => updateDisplay(e.NewValue); From cccb47e6e04e956dc4cfe73dfdff8c2bdc993526 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jul 2020 11:29:23 +0900 Subject: [PATCH 42/78] Add user cover background to expanded version of score panel --- osu.Game/Screens/Ranking/ScorePanel.cs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 9633f5c533..5da432d5b2 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -13,6 +13,7 @@ using osu.Framework.Input.Events; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Contracted; using osu.Game.Screens.Ranking.Expanded; +using osu.Game.Users; using osuTK; using osuTK.Graphics; @@ -142,7 +143,16 @@ namespace osu.Game.Screens.Ranking CornerRadius = 20, CornerExponent = 2.5f, Masking = true, - Child = middleLayerBackground = new Box { RelativeSizeAxes = Axes.Both } + Children = new[] + { + middleLayerBackground = new Box { RelativeSizeAxes = Axes.Both }, + new UserCoverBackground + { + RelativeSizeAxes = Axes.Both, + User = Score.User, + Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0.5f), Color4Extensions.FromHex("#444").Opacity(0)) + }, + } }, middleLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both } } @@ -155,18 +165,10 @@ namespace osu.Game.Screens.Ranking { base.LoadComplete(); - if (state == PanelState.Expanded) - { - topLayerBackground.FadeColour(expanded_top_layer_colour); - middleLayerBackground.FadeColour(expanded_middle_layer_colour); - } - else - { - topLayerBackground.FadeColour(contracted_top_layer_colour); - middleLayerBackground.FadeColour(contracted_middle_layer_colour); - } - updateState(); + + topLayerBackground.FinishTransforms(false, nameof(Colour)); + middleLayerBackground.FinishTransforms(false, nameof(Colour)); } private PanelState state = PanelState.Contracted; From aca4110e36d03568e1ca2ceadeaf3df42a41093e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 22 Jul 2020 12:47:53 +0900 Subject: [PATCH 43/78] Use existing star difficulty if non-local beatmap/ruleset --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index b469ca78fb..d94e04a79b 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -209,7 +209,8 @@ namespace osu.Game.Beatmaps // Difficulty can only be computed if the beatmap and ruleset are locally available. if (beatmapInfo.ID == 0 || rulesetInfo.ID == null) { - existingDifficulty = new StarDifficulty(0); + // If not, fall back to the existing star difficulty (e.g. from an online source). + existingDifficulty = new StarDifficulty(beatmapInfo.StarDifficulty); key = default; return true; From 6b7f05740e51c77790295a0ba882c8b45d379bb2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 22 Jul 2020 12:48:12 +0900 Subject: [PATCH 44/78] Fix potential missing ruleset --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index d94e04a79b..a9f34acd14 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -176,6 +176,9 @@ namespace osu.Game.Beatmaps /// The . private StarDifficulty computeDifficulty(in DifficultyCacheLookup key, BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo) { + // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. + rulesetInfo ??= beatmapInfo.Ruleset; + try { var ruleset = rulesetInfo.CreateInstance(); From ac602846df9d6a6be5b70672b537fe126cd0274e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jul 2020 16:37:24 +0900 Subject: [PATCH 45/78] Expose balance and sample loading methods in DrawableHitObject --- .../Objects/Drawables/DrawableHitObject.cs | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index b633cb0860..f275153ce3 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Objects.Drawables if (Result == null) throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}."); - loadSamples(); + LoadSamples(); } protected override void LoadComplete() @@ -145,14 +145,14 @@ namespace osu.Game.Rulesets.Objects.Drawables } samplesBindable = HitObject.SamplesBindable.GetBoundCopy(); - samplesBindable.CollectionChanged += (_, __) => loadSamples(); + samplesBindable.CollectionChanged += (_, __) => LoadSamples(); apply(HitObject); updateState(ArmedState.Idle, true); } - private void loadSamples() + protected virtual void LoadSamples() { if (Samples != null) { @@ -353,17 +353,32 @@ namespace osu.Game.Rulesets.Objects.Drawables [Resolved(canBeNull: true)] private GameplayClock gameplayClock { get; set; } + /// + /// Calculate the position to be used for sample playback at a specified X position (0..1). + /// + /// The lookup X position. Generally should be . + /// + protected double CalculateSamplePlaybackBalance(double position) + { + const float balance_adjust_amount = 0.4f; + + return balance_adjust_amount * (userPositionalHitSounds.Value ? position - 0.5f : 0); + } + + /// + /// Whether samples should currently be playing. Will be false during seek operations. + /// + protected bool ShouldPlaySamples => gameplayClock?.IsSeeking != true; + /// /// Plays all the hit sounds for this . /// This is invoked automatically when this is hit. /// public virtual void PlaySamples() { - const float balance_adjust_amount = 0.4f; - - if (Samples != null && gameplayClock?.IsSeeking != true) + if (Samples != null && ShouldPlaySamples) { - Samples.Balance.Value = balance_adjust_amount * (userPositionalHitSounds.Value ? SamplePlaybackPosition - 0.5f : 0); + Samples.Balance.Value = CalculateSamplePlaybackBalance(SamplePlaybackPosition); Samples.Play(); } } From 2126f6bffc9d613706e12c4ef153c1fb7cb567ab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jul 2020 16:37:38 +0900 Subject: [PATCH 46/78] Add slider "sliding" sample support --- .../Objects/Drawables/DrawableSlider.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 72502c02cd..5059ec1231 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osuTK; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; @@ -11,6 +12,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Skinning; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Scoring; using osuTK.Graphics; using osu.Game.Skinning; @@ -81,6 +83,41 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables foreach (var drawableHitObject in NestedHitObjects) drawableHitObject.AccentColour.Value = colour.NewValue; }, true); + + Tracking.BindValueChanged(updateSlidingSample); + } + + private SkinnableSound slidingSample; + + protected override void LoadSamples() + { + base.LoadSamples(); + + slidingSample?.Expire(); + + var firstSample = HitObject.Samples.FirstOrDefault(); + + if (firstSample != null) + { + var clone = HitObject.SampleControlPoint.ApplyTo(firstSample); + clone.Name = "sliderslide"; + + AddInternal(slidingSample = new SkinnableSound(clone) + { + Looping = true + }); + } + } + + private void updateSlidingSample(ValueChangedEvent tracking) + { + // note that samples will not start playing if exiting a seek operation in the middle of a slider. + // may be something we want to address at a later point, but not so easy to make happen right now + // (SkinnableSound would need to expose whether the sample is already playing and this logic would need to run in Update). + if (tracking.NewValue && ShouldPlaySamples) + slidingSample?.Play(); + else + slidingSample?.Stop(); } protected override void AddNestedHitObject(DrawableHitObject hitObject) @@ -156,6 +193,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Tracking.Value = Ball.Tracking; + if (Tracking.Value && slidingSample != null) + // keep the sliding sample playing at the current tracking position + slidingSample.Balance.Value = CalculateSamplePlaybackBalance(Ball.X / OsuPlayfield.BASE_SIZE.X); + double completionProgress = Math.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1); Ball.UpdateProgress(completionProgress); From 0957c5f74ce0bdf1e3fc6524d18d0021780abb86 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 22 Jul 2020 18:29:50 +0900 Subject: [PATCH 47/78] Re-namespace multiplayer requests/responses --- .../Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs | 1 - .../Requests/Responses => Multiplayer}/APICreatedRoom.cs | 3 +-- osu.Game/Online/{API => Multiplayer}/APIPlaylistBeatmap.cs | 2 +- .../{API/Requests/Responses => Multiplayer}/APIScoreToken.cs | 2 +- .../{API/Requests => Multiplayer}/CreateRoomRequest.cs | 5 ++--- .../{API/Requests => Multiplayer}/CreateRoomScoreRequest.cs | 4 ++-- .../Requests => Multiplayer}/GetRoomPlaylistScoresRequest.cs | 3 ++- .../Online/{API/Requests => Multiplayer}/GetRoomRequest.cs | 4 ++-- .../{API/Requests => Multiplayer}/GetRoomScoresRequest.cs | 3 ++- .../Online/{API/Requests => Multiplayer}/GetRoomsRequest.cs | 4 ++-- .../Online/{API/Requests => Multiplayer}/JoinRoomRequest.cs | 4 ++-- .../Online/{API/Requests => Multiplayer}/PartRoomRequest.cs | 4 ++-- osu.Game/Online/{API => Multiplayer}/RoomScore.cs | 4 ++-- .../{API/Requests => Multiplayer}/SubmitRoomScoreRequest.cs | 3 ++- osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs | 1 - osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs | 1 - osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs | 1 - osu.Game/Screens/Multi/RoomManager.cs | 1 - 18 files changed, 23 insertions(+), 27 deletions(-) rename osu.Game/Online/{API/Requests/Responses => Multiplayer}/APICreatedRoom.cs (78%) rename osu.Game/Online/{API => Multiplayer}/APIPlaylistBeatmap.cs (94%) rename osu.Game/Online/{API/Requests/Responses => Multiplayer}/APIScoreToken.cs (85%) rename osu.Game/Online/{API/Requests => Multiplayer}/CreateRoomRequest.cs (86%) rename osu.Game/Online/{API/Requests => Multiplayer}/CreateRoomScoreRequest.cs (90%) rename osu.Game/Online/{API/Requests => Multiplayer}/GetRoomPlaylistScoresRequest.cs (92%) rename osu.Game/Online/{API/Requests => Multiplayer}/GetRoomRequest.cs (84%) rename osu.Game/Online/{API/Requests => Multiplayer}/GetRoomScoresRequest.cs (89%) rename osu.Game/Online/{API/Requests => Multiplayer}/GetRoomsRequest.cs (94%) rename osu.Game/Online/{API/Requests => Multiplayer}/JoinRoomRequest.cs (90%) rename osu.Game/Online/{API/Requests => Multiplayer}/PartRoomRequest.cs (90%) rename osu.Game/Online/{API => Multiplayer}/RoomScore.cs (97%) rename osu.Game/Online/{API/Requests => Multiplayer}/SubmitRoomScoreRequest.cs (95%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs index 9fc7c336cb..0da1e11fee 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Threading.Tasks; using NUnit.Framework; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.Multiplayer; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game/Online/API/Requests/Responses/APICreatedRoom.cs b/osu.Game/Online/Multiplayer/APICreatedRoom.cs similarity index 78% rename from osu.Game/Online/API/Requests/Responses/APICreatedRoom.cs rename to osu.Game/Online/Multiplayer/APICreatedRoom.cs index a554101bc7..2a3bb39647 100644 --- a/osu.Game/Online/API/Requests/Responses/APICreatedRoom.cs +++ b/osu.Game/Online/Multiplayer/APICreatedRoom.cs @@ -2,9 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using Newtonsoft.Json; -using osu.Game.Online.Multiplayer; -namespace osu.Game.Online.API.Requests.Responses +namespace osu.Game.Online.Multiplayer { public class APICreatedRoom : Room { diff --git a/osu.Game/Online/API/APIPlaylistBeatmap.cs b/osu.Game/Online/Multiplayer/APIPlaylistBeatmap.cs similarity index 94% rename from osu.Game/Online/API/APIPlaylistBeatmap.cs rename to osu.Game/Online/Multiplayer/APIPlaylistBeatmap.cs index 4f7786e880..98972ef36d 100644 --- a/osu.Game/Online/API/APIPlaylistBeatmap.cs +++ b/osu.Game/Online/Multiplayer/APIPlaylistBeatmap.cs @@ -6,7 +6,7 @@ using osu.Game.Beatmaps; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; -namespace osu.Game.Online.API +namespace osu.Game.Online.Multiplayer { public class APIPlaylistBeatmap : APIBeatmap { diff --git a/osu.Game/Online/API/Requests/Responses/APIScoreToken.cs b/osu.Game/Online/Multiplayer/APIScoreToken.cs similarity index 85% rename from osu.Game/Online/API/Requests/Responses/APIScoreToken.cs rename to osu.Game/Online/Multiplayer/APIScoreToken.cs index 1d2465bedf..1f0063d94e 100644 --- a/osu.Game/Online/API/Requests/Responses/APIScoreToken.cs +++ b/osu.Game/Online/Multiplayer/APIScoreToken.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; -namespace osu.Game.Online.API.Requests.Responses +namespace osu.Game.Online.Multiplayer { public class APIScoreToken { diff --git a/osu.Game/Online/API/Requests/CreateRoomRequest.cs b/osu.Game/Online/Multiplayer/CreateRoomRequest.cs similarity index 86% rename from osu.Game/Online/API/Requests/CreateRoomRequest.cs rename to osu.Game/Online/Multiplayer/CreateRoomRequest.cs index c848c55cc6..dcb4ed51ea 100644 --- a/osu.Game/Online/API/Requests/CreateRoomRequest.cs +++ b/osu.Game/Online/Multiplayer/CreateRoomRequest.cs @@ -4,10 +4,9 @@ using System.Net.Http; using Newtonsoft.Json; using osu.Framework.IO.Network; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.API; -namespace osu.Game.Online.API.Requests +namespace osu.Game.Online.Multiplayer { public class CreateRoomRequest : APIRequest { diff --git a/osu.Game/Online/API/Requests/CreateRoomScoreRequest.cs b/osu.Game/Online/Multiplayer/CreateRoomScoreRequest.cs similarity index 90% rename from osu.Game/Online/API/Requests/CreateRoomScoreRequest.cs rename to osu.Game/Online/Multiplayer/CreateRoomScoreRequest.cs index e6246b4f1f..f973f96b37 100644 --- a/osu.Game/Online/API/Requests/CreateRoomScoreRequest.cs +++ b/osu.Game/Online/Multiplayer/CreateRoomScoreRequest.cs @@ -3,9 +3,9 @@ using System.Net.Http; using osu.Framework.IO.Network; -using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.API; -namespace osu.Game.Online.API.Requests +namespace osu.Game.Online.Multiplayer { public class CreateRoomScoreRequest : APIRequest { diff --git a/osu.Game/Online/API/Requests/GetRoomPlaylistScoresRequest.cs b/osu.Game/Online/Multiplayer/GetRoomPlaylistScoresRequest.cs similarity index 92% rename from osu.Game/Online/API/Requests/GetRoomPlaylistScoresRequest.cs rename to osu.Game/Online/Multiplayer/GetRoomPlaylistScoresRequest.cs index 38f852870b..833a761f42 100644 --- a/osu.Game/Online/API/Requests/GetRoomPlaylistScoresRequest.cs +++ b/osu.Game/Online/Multiplayer/GetRoomPlaylistScoresRequest.cs @@ -3,8 +3,9 @@ using System.Collections.Generic; using Newtonsoft.Json; +using osu.Game.Online.API; -namespace osu.Game.Online.API.Requests +namespace osu.Game.Online.Multiplayer { public class GetRoomPlaylistScoresRequest : APIRequest { diff --git a/osu.Game/Online/API/Requests/GetRoomRequest.cs b/osu.Game/Online/Multiplayer/GetRoomRequest.cs similarity index 84% rename from osu.Game/Online/API/Requests/GetRoomRequest.cs rename to osu.Game/Online/Multiplayer/GetRoomRequest.cs index 531e1857de..2907b49f1d 100644 --- a/osu.Game/Online/API/Requests/GetRoomRequest.cs +++ b/osu.Game/Online/Multiplayer/GetRoomRequest.cs @@ -1,9 +1,9 @@ // 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.Online.Multiplayer; +using osu.Game.Online.API; -namespace osu.Game.Online.API.Requests +namespace osu.Game.Online.Multiplayer { public class GetRoomRequest : APIRequest { diff --git a/osu.Game/Online/API/Requests/GetRoomScoresRequest.cs b/osu.Game/Online/Multiplayer/GetRoomScoresRequest.cs similarity index 89% rename from osu.Game/Online/API/Requests/GetRoomScoresRequest.cs rename to osu.Game/Online/Multiplayer/GetRoomScoresRequest.cs index eb53369d18..bc913030dd 100644 --- a/osu.Game/Online/API/Requests/GetRoomScoresRequest.cs +++ b/osu.Game/Online/Multiplayer/GetRoomScoresRequest.cs @@ -2,9 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; -namespace osu.Game.Online.API.Requests +namespace osu.Game.Online.Multiplayer { public class GetRoomScoresRequest : APIRequest> { diff --git a/osu.Game/Online/API/Requests/GetRoomsRequest.cs b/osu.Game/Online/Multiplayer/GetRoomsRequest.cs similarity index 94% rename from osu.Game/Online/API/Requests/GetRoomsRequest.cs rename to osu.Game/Online/Multiplayer/GetRoomsRequest.cs index c47ed20909..64e0386f77 100644 --- a/osu.Game/Online/API/Requests/GetRoomsRequest.cs +++ b/osu.Game/Online/Multiplayer/GetRoomsRequest.cs @@ -4,10 +4,10 @@ using System.Collections.Generic; using Humanizer; using osu.Framework.IO.Network; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.API; using osu.Game.Screens.Multi.Lounge.Components; -namespace osu.Game.Online.API.Requests +namespace osu.Game.Online.Multiplayer { public class GetRoomsRequest : APIRequest> { diff --git a/osu.Game/Online/API/Requests/JoinRoomRequest.cs b/osu.Game/Online/Multiplayer/JoinRoomRequest.cs similarity index 90% rename from osu.Game/Online/API/Requests/JoinRoomRequest.cs rename to osu.Game/Online/Multiplayer/JoinRoomRequest.cs index b0808afa45..74375af856 100644 --- a/osu.Game/Online/API/Requests/JoinRoomRequest.cs +++ b/osu.Game/Online/Multiplayer/JoinRoomRequest.cs @@ -3,9 +3,9 @@ using System.Net.Http; using osu.Framework.IO.Network; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.API; -namespace osu.Game.Online.API.Requests +namespace osu.Game.Online.Multiplayer { public class JoinRoomRequest : APIRequest { diff --git a/osu.Game/Online/API/Requests/PartRoomRequest.cs b/osu.Game/Online/Multiplayer/PartRoomRequest.cs similarity index 90% rename from osu.Game/Online/API/Requests/PartRoomRequest.cs rename to osu.Game/Online/Multiplayer/PartRoomRequest.cs index c988cd5c9e..54bb005d96 100644 --- a/osu.Game/Online/API/Requests/PartRoomRequest.cs +++ b/osu.Game/Online/Multiplayer/PartRoomRequest.cs @@ -3,9 +3,9 @@ using System.Net.Http; using osu.Framework.IO.Network; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.API; -namespace osu.Game.Online.API.Requests +namespace osu.Game.Online.Multiplayer { public class PartRoomRequest : APIRequest { diff --git a/osu.Game/Online/API/RoomScore.cs b/osu.Game/Online/Multiplayer/RoomScore.cs similarity index 97% rename from osu.Game/Online/API/RoomScore.cs rename to osu.Game/Online/Multiplayer/RoomScore.cs index 3c7f8c9833..97f378856f 100644 --- a/osu.Game/Online/API/RoomScore.cs +++ b/osu.Game/Online/Multiplayer/RoomScore.cs @@ -6,13 +6,13 @@ using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Converters; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.API; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Users; -namespace osu.Game.Online.API +namespace osu.Game.Online.Multiplayer { public class RoomScore { diff --git a/osu.Game/Online/API/Requests/SubmitRoomScoreRequest.cs b/osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs similarity index 95% rename from osu.Game/Online/API/Requests/SubmitRoomScoreRequest.cs rename to osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs index 8eb2952159..f725ea5dc9 100644 --- a/osu.Game/Online/API/Requests/SubmitRoomScoreRequest.cs +++ b/osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs @@ -4,9 +4,10 @@ using System.Net.Http; using Newtonsoft.Json; using osu.Framework.IO.Network; +using osu.Game.Online.API; using osu.Game.Scoring; -namespace osu.Game.Online.API.Requests +namespace osu.Game.Online.Multiplayer { public class SubmitRoomScoreRequest : APIRequest { diff --git a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs index 571bbde716..1afbf5c32a 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; using osu.Game.Online.Multiplayer; diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs index cf0197d26b..c2381fe219 100644 --- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs +++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs @@ -10,7 +10,6 @@ using osu.Framework.Bindables; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.Multiplayer; using osu.Game.Rulesets; using osu.Game.Scoring; diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs index 5cafc974f1..f367d44347 100644 --- a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs +++ b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.Multiplayer; using osu.Game.Scoring; using osu.Game.Screens.Ranking; diff --git a/osu.Game/Screens/Multi/RoomManager.cs b/osu.Game/Screens/Multi/RoomManager.cs index 491be2e946..2a96fa536d 100644 --- a/osu.Game/Screens/Multi/RoomManager.cs +++ b/osu.Game/Screens/Multi/RoomManager.cs @@ -14,7 +14,6 @@ using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Online; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.Multiplayer; using osu.Game.Rulesets; using osu.Game.Screens.Multi.Lounge.Components; From e423630b7cbec15c0457089513ff0af85822591b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 22 Jul 2020 18:37:00 +0900 Subject: [PATCH 48/78] Rename RoomScore -> MultiplayerScore --- .../Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs | 4 ++-- osu.Game/Online/Multiplayer/GetRoomPlaylistScoresRequest.cs | 2 +- .../Online/Multiplayer/{RoomScore.cs => MultiplayerScore.cs} | 2 +- osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename osu.Game/Online/Multiplayer/{RoomScore.cs => MultiplayerScore.cs} (98%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs index 0da1e11fee..0023866124 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs @@ -64,11 +64,11 @@ namespace osu.Game.Tests.Visual.Multiplayer private void bindHandler(double delay = 0) { - var roomScores = new List(); + var roomScores = new List(); for (int i = 0; i < 10; i++) { - roomScores.Add(new RoomScore + roomScores.Add(new MultiplayerScore { ID = i, Accuracy = 0.9 - 0.01 * i, diff --git a/osu.Game/Online/Multiplayer/GetRoomPlaylistScoresRequest.cs b/osu.Game/Online/Multiplayer/GetRoomPlaylistScoresRequest.cs index 833a761f42..3d3bd20ff3 100644 --- a/osu.Game/Online/Multiplayer/GetRoomPlaylistScoresRequest.cs +++ b/osu.Game/Online/Multiplayer/GetRoomPlaylistScoresRequest.cs @@ -24,6 +24,6 @@ namespace osu.Game.Online.Multiplayer public class RoomPlaylistScores { [JsonProperty("scores")] - public List Scores { get; set; } + public List Scores { get; set; } } } diff --git a/osu.Game/Online/Multiplayer/RoomScore.cs b/osu.Game/Online/Multiplayer/MultiplayerScore.cs similarity index 98% rename from osu.Game/Online/Multiplayer/RoomScore.cs rename to osu.Game/Online/Multiplayer/MultiplayerScore.cs index 97f378856f..3bbf19b11f 100644 --- a/osu.Game/Online/Multiplayer/RoomScore.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerScore.cs @@ -14,7 +14,7 @@ using osu.Game.Users; namespace osu.Game.Online.Multiplayer { - public class RoomScore + public class MultiplayerScore { [JsonProperty("id")] public int ID { get; set; } diff --git a/osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs b/osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs index f725ea5dc9..d31aef2ea5 100644 --- a/osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs +++ b/osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs @@ -9,7 +9,7 @@ using osu.Game.Scoring; namespace osu.Game.Online.Multiplayer { - public class SubmitRoomScoreRequest : APIRequest + public class SubmitRoomScoreRequest : APIRequest { private readonly int scoreId; private readonly int roomId; From 53a9ac3c1aa160bd1ccd8a23aa3833bd5f014e9b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jul 2020 19:06:39 +0900 Subject: [PATCH 49/78] Fix slider ball rotation being applied to follow circle and specular layer --- .../Objects/Drawables/Pieces/SliderBall.cs | 18 ++++-------- .../Skinning/LegacySliderBall.cs | 28 +++++++++++++++++-- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs index 395c76a233..b87e112d10 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private readonly Slider slider; private readonly Drawable followCircle; private readonly DrawableSlider drawableSlider; - private readonly CircularContainer ball; + private readonly Drawable ball; public SliderBall(Slider slider, DrawableSlider drawableSlider = null) { @@ -54,19 +54,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Alpha = 0, Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle()), }, - ball = new CircularContainer + ball = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBall), _ => new DefaultSliderBall()) { - Masking = true, - RelativeSizeAxes = Axes.Both, - Origin = Anchor.Centre, Anchor = Anchor.Centre, - Alpha = 1, - Child = new Container - { - RelativeSizeAxes = Axes.Both, - Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBall), _ => new DefaultSliderBall()), - } - } + Origin = Anchor.Centre, + }, }; } @@ -187,7 +179,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces return; Position = newPos; - Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI); + ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI); lastPosition = newPos; } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs index b4ed75d97c..0f586034d5 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs @@ -15,6 +15,9 @@ namespace osu.Game.Rulesets.Osu.Skinning { private readonly Drawable animationContent; + private Sprite layerNd; + private Sprite layerSpec; + public LegacySliderBall(Drawable animationContent) { this.animationContent = animationContent; @@ -29,18 +32,37 @@ namespace osu.Game.Rulesets.Osu.Skinning InternalChildren = new[] { - new Sprite + layerNd = new Sprite { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Texture = skin.GetTexture("sliderb-nd"), Colour = new Color4(5, 5, 5, 255), }, - animationContent, - new Sprite + animationContent.With(d => { + d.Anchor = Anchor.Centre; + d.Origin = Anchor.Centre; + }), + layerSpec = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Texture = skin.GetTexture("sliderb-spec"), Blending = BlendingParameters.Additive, }, }; } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + //undo rotation on layers which should not be rotated. + float appliedRotation = Parent.Rotation; + + layerNd.Rotation = -appliedRotation; + layerSpec.Rotation = -appliedRotation; + } } } From 113fac84ddf195b10d1ae3a9b1cd1e437c94d0ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jul 2020 21:14:04 +0900 Subject: [PATCH 50/78] Fix circle container type --- osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs index b87e112d10..07dc6021c9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs @@ -184,7 +184,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces lastPosition = newPos; } - private class FollowCircleContainer : Container + private class FollowCircleContainer : CircularContainer { public override bool HandlePositionalInput => true; } From 4102dae999cb7f63294b033898885d50afbc799b Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 22 Jul 2020 21:45:27 +0200 Subject: [PATCH 51/78] Revert commit 939441ae --- osu.Desktop/Windows/GameplayWinKeyHandler.cs | 14 +++++++------- osu.Game/Configuration/SessionStatics.cs | 4 +--- osu.Game/Screens/Play/Player.cs | 2 +- .../Screens/Play/ScreenSuspensionHandler.cs | 19 ++----------------- 4 files changed, 11 insertions(+), 28 deletions(-) diff --git a/osu.Desktop/Windows/GameplayWinKeyHandler.cs b/osu.Desktop/Windows/GameplayWinKeyHandler.cs index d5ef89c680..4f74a4f492 100644 --- a/osu.Desktop/Windows/GameplayWinKeyHandler.cs +++ b/osu.Desktop/Windows/GameplayWinKeyHandler.cs @@ -11,26 +11,26 @@ namespace osu.Desktop.Windows { public class GameplayWinKeyHandler : Component { + private Bindable allowScreenSuspension; private Bindable disableWinKey; - private Bindable disableWinKeySetting; private GameHost host; [BackgroundDependencyLoader] - private void load(GameHost host, OsuConfigManager config, SessionStatics statics) + private void load(GameHost host, OsuConfigManager config) { this.host = host; - disableWinKey = statics.GetBindable(Static.DisableWindowsKey); - disableWinKey.ValueChanged += toggleWinKey; + allowScreenSuspension = host.AllowScreenSuspension.GetBoundCopy(); + allowScreenSuspension.ValueChanged += toggleWinKey; - disableWinKeySetting = config.GetBindable(OsuSetting.GameplayDisableWinKey); - disableWinKeySetting.BindValueChanged(t => disableWinKey.TriggerChange(), true); + disableWinKey = config.GetBindable(OsuSetting.GameplayDisableWinKey); + disableWinKey.BindValueChanged(t => allowScreenSuspension.TriggerChange(), true); } private void toggleWinKey(ValueChangedEvent e) { - if (e.NewValue && disableWinKeySetting.Value) + if (!e.NewValue && disableWinKey.Value) host.InputThread.Scheduler.Add(WindowsKey.Disable); else host.InputThread.Scheduler.Add(WindowsKey.Enable); diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index 7aad79b5ad..40b2adb867 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -12,14 +12,12 @@ namespace osu.Game.Configuration { Set(Static.LoginOverlayDisplayed, false); Set(Static.MutedAudioNotificationShownOnce, false); - Set(Static.DisableWindowsKey, false); } } public enum Static { LoginOverlayDisplayed, - MutedAudioNotificationShownOnce, - DisableWindowsKey + MutedAudioNotificationShownOnce } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index e0721d55f7..541275cf55 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -181,7 +181,7 @@ namespace osu.Game.Screens.Play InternalChild = GameplayClockContainer = new GameplayClockContainer(Beatmap.Value, Mods.Value, DrawableRuleset.GameplayStartTime); AddInternal(gameplayBeatmap = new GameplayBeatmap(playableBeatmap)); - AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer, DrawableRuleset.HasReplayLoaded)); + AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); dependencies.CacheAs(gameplayBeatmap); diff --git a/osu.Game/Screens/Play/ScreenSuspensionHandler.cs b/osu.Game/Screens/Play/ScreenSuspensionHandler.cs index 6865db5a5e..8585a5c309 100644 --- a/osu.Game/Screens/Play/ScreenSuspensionHandler.cs +++ b/osu.Game/Screens/Play/ScreenSuspensionHandler.cs @@ -8,7 +8,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Platform; -using osu.Game.Configuration; namespace osu.Game.Screens.Play { @@ -19,18 +18,13 @@ namespace osu.Game.Screens.Play { private readonly GameplayClockContainer gameplayClockContainer; private Bindable isPaused; - private readonly Bindable hasReplayLoaded; [Resolved] private GameHost host { get; set; } - [Resolved] - private SessionStatics statics { get; set; } - - public ScreenSuspensionHandler([NotNull] GameplayClockContainer gameplayClockContainer, Bindable hasReplayLoaded) + public ScreenSuspensionHandler([NotNull] GameplayClockContainer gameplayClockContainer) { this.gameplayClockContainer = gameplayClockContainer ?? throw new ArgumentNullException(nameof(gameplayClockContainer)); - this.hasReplayLoaded = hasReplayLoaded.GetBoundCopy(); } protected override void LoadComplete() @@ -42,12 +36,7 @@ namespace osu.Game.Screens.Play Debug.Assert(host.AllowScreenSuspension.Value); isPaused = gameplayClockContainer.IsPaused.GetBoundCopy(); - isPaused.BindValueChanged(paused => - { - host.AllowScreenSuspension.Value = paused.NewValue; - statics.Set(Static.DisableWindowsKey, !paused.NewValue && !hasReplayLoaded.Value); - }, true); - hasReplayLoaded.BindValueChanged(_ => isPaused.TriggerChange(), true); + isPaused.BindValueChanged(paused => host.AllowScreenSuspension.Value = paused.NewValue, true); } protected override void Dispose(bool isDisposing) @@ -55,13 +44,9 @@ namespace osu.Game.Screens.Play base.Dispose(isDisposing); isPaused?.UnbindAll(); - hasReplayLoaded.UnbindAll(); if (host != null) - { host.AllowScreenSuspension.Value = true; - statics.Set(Static.DisableWindowsKey, false); - } } } } From acff270e969cde58d12893fc891351d3d06afdbd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jul 2020 19:14:18 +0900 Subject: [PATCH 52/78] Fix failing test by moving slider closer --- osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs index c3b4d2625e..854626d362 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs @@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.Tests const double time_slider = 1500; const double time_circle = 1510; Vector2 positionCircle = Vector2.Zero; - Vector2 positionSlider = new Vector2(80); + Vector2 positionSlider = new Vector2(30); var hitObjects = new List { From 5e6adfff99b1b348897ab4606aef7f910016560c Mon Sep 17 00:00:00 2001 From: Lucas A Date: Thu, 23 Jul 2020 12:45:14 +0200 Subject: [PATCH 53/78] Disable windows key only while in gameplay. --- osu.Desktop/OsuGameDesktop.cs | 2 +- osu.Desktop/Windows/GameplayWinKeyHandler.cs | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index d05a4af126..6eefee3b50 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -101,7 +101,7 @@ namespace osu.Desktop LoadComponentAsync(new DiscordRichPresence(), Add); if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) - LoadComponentAsync(new GameplayWinKeyHandler(), Add); + LoadComponentAsync(new GameplayWinKeyHandler(ScreenStack), Add); } protected override void ScreenChanged(IScreen lastScreen, IScreen newScreen) diff --git a/osu.Desktop/Windows/GameplayWinKeyHandler.cs b/osu.Desktop/Windows/GameplayWinKeyHandler.cs index 4f74a4f492..96154356d0 100644 --- a/osu.Desktop/Windows/GameplayWinKeyHandler.cs +++ b/osu.Desktop/Windows/GameplayWinKeyHandler.cs @@ -1,11 +1,14 @@ // 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Game.Configuration; +using osu.Game.Screens; +using osu.Game.Screens.Play; namespace osu.Desktop.Windows { @@ -14,8 +17,16 @@ namespace osu.Desktop.Windows private Bindable allowScreenSuspension; private Bindable disableWinKey; + private readonly OsuScreenStack screenStack; private GameHost host; + private Type currentScreenType => screenStack.CurrentScreen?.GetType(); + + public GameplayWinKeyHandler(OsuScreenStack stack) + { + screenStack = stack; + } + [BackgroundDependencyLoader] private void load(GameHost host, OsuConfigManager config) { @@ -30,7 +41,9 @@ namespace osu.Desktop.Windows private void toggleWinKey(ValueChangedEvent e) { - if (!e.NewValue && disableWinKey.Value) + var isPlayer = typeof(Player).IsAssignableFrom(currentScreenType) && currentScreenType != typeof(ReplayPlayer); + + if (!e.NewValue && disableWinKey.Value && isPlayer) host.InputThread.Scheduler.Add(WindowsKey.Disable); else host.InputThread.Scheduler.Add(WindowsKey.Enable); From f883cb85d72ff2f98ec87a1f207e239b805e2c8b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 23 Jul 2020 21:24:31 +0900 Subject: [PATCH 54/78] Null out the sample too --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 5059ec1231..07f40f763b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -94,6 +94,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.LoadSamples(); slidingSample?.Expire(); + slidingSample = null; var firstSample = HitObject.Samples.FirstOrDefault(); From d0b35d7b32895ff3f988c0f6e85fa86eaacaad0f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jul 2020 22:13:37 +0900 Subject: [PATCH 55/78] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index c0c75b8d71..e5b0245dd0 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index e8c333b6b1..5af28ae11a 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 8d1b837995..4a94ec33d8 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 76284a0f018f2c8c3a502db24360081e0c1f5996 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 23 Jul 2020 23:18:43 +0900 Subject: [PATCH 56/78] Move cancellation out of condition --- .../Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index d5aeecae04..1b5b448e1f 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -188,12 +188,11 @@ namespace osu.Game.Screens.Select.Carousel if (Item.State.Value != CarouselItemState.Collapsed && Alpha == 0) starCounter.ReplayAnimation(); - if (Item.State.Value == CarouselItemState.Collapsed) - starDifficultyCancellationSource?.Cancel(); - else - { - starDifficultyCancellationSource?.Cancel(); + starDifficultyCancellationSource?.Cancel(); + // Only compute difficulty when the item is visible. + if (Item.State.Value != CarouselItemState.Collapsed) + { // We've potentially cancelled the computation above so a new bindable is required. starDifficultyBindable = difficultyManager.GetTrackedBindable(beatmap, (starDifficultyCancellationSource = new CancellationTokenSource()).Token); starDifficultyBindable.BindValueChanged(d => starCounter.Current = (float)d.NewValue.Stars, true); From f75f1231b7f2300b260e25e7dc8f2d4d273b2bc7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jul 2020 10:41:09 +0900 Subject: [PATCH 57/78] Invert conditional for readability --- osu.Game/Graphics/Cursor/MenuCursorContainer.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs index 02bfb3fad6..3015c44613 100644 --- a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs +++ b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs @@ -58,10 +58,11 @@ namespace osu.Game.Graphics.Cursor foreach (var d in inputManager.HoveredDrawables) { - if (!(d is IProvideCursor p) || !p.ProvidingUserCursor) continue; - - newTarget = p; - break; + if (d is IProvideCursor p && p.ProvidingUserCursor) + { + newTarget = p; + break; + } } if (currentTarget == newTarget) From 264bd7ced1c8a8caab663eebba2114bfefa766a9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 24 Jul 2020 13:38:53 +0900 Subject: [PATCH 58/78] Apply general refactoring from review --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index a9f34acd14..12d472e8c6 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -24,7 +24,7 @@ namespace osu.Game.Beatmaps // Too many simultaneous updates can lead to stutters. One thread seems to work fine for song select display purposes. private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(1, nameof(BeatmapDifficultyManager)); - // A cache that keeps references to BeatmapInfos for 60sec. + // A permanent cache to prevent re-computations. private readonly ConcurrentDictionary difficultyCache = new ConcurrentDictionary(); // All bindables that should be updated along with the current ruleset + mods. @@ -48,29 +48,29 @@ namespace osu.Game.Beatmaps } /// - /// Retrieves an containing the star difficulty of a with a given and combination. + /// Retrieves a bindable containing the star difficulty of a with a given and combination. /// /// - /// This will not update to follow the currently-selected ruleset and mods. + /// The bindable will not update to follow the currently-selected ruleset and mods. /// /// The to get the difficulty of. /// The to get the difficulty with. /// The s to get the difficulty with. /// An optional which stops updating the star difficulty for the given . - /// An that is updated to contain the star difficulty when it becomes available. + /// A bindable that is updated to contain the star difficulty when it becomes available. public IBindable GetUntrackedBindable([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null, CancellationToken cancellationToken = default) => createBindable(beatmapInfo, rulesetInfo, mods, cancellationToken); /// - /// Retrieves a containing the star difficulty of a that follows the user's currently-selected ruleset and mods. + /// Retrieves a bindable containing the star difficulty of a that follows the user's currently-selected ruleset and mods. /// /// - /// Ensure to hold a local reference of the returned in order to receive value-changed events. + /// Ensure to hold a local reference of the returned bindable in order to receive value-changed events. /// /// The to get the difficulty of. /// An optional which stops updating the star difficulty for the given . - /// An that is updated to contain the star difficulty when it becomes available, or when the currently-selected ruleset and mods change. + /// A bindable that is updated to contain the star difficulty when it becomes available, or when the currently-selected ruleset and mods change. public IBindable GetTrackedBindable([NotNull] BeatmapInfo beatmapInfo, CancellationToken cancellationToken = default) { var bindable = createBindable(beatmapInfo, currentRuleset.Value, currentMods.Value, cancellationToken); @@ -89,7 +89,7 @@ namespace osu.Game.Beatmaps public async Task GetDifficultyAsync([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null, CancellationToken cancellationToken = default) { - if (tryGetGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) + if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) return existing; return await Task.Factory.StartNew(() => computeDifficulty(key, beatmapInfo, rulesetInfo), cancellationToken, @@ -105,7 +105,7 @@ namespace osu.Game.Beatmaps /// The . public StarDifficulty GetDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null) { - if (tryGetGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) + if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) return existing; return computeDifficulty(key, beatmapInfo, rulesetInfo); @@ -204,7 +204,7 @@ namespace osu.Game.Beatmaps /// The existing difficulty value, if present. /// The key that was used to perform this lookup. This can be further used to query . /// Whether an existing difficulty was found. - private bool tryGetGetExisting(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IReadOnlyList mods, out StarDifficulty existingDifficulty, out DifficultyCacheLookup key) + private bool tryGetExisting(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IReadOnlyList mods, out StarDifficulty existingDifficulty, out DifficultyCacheLookup key) { // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. rulesetInfo ??= beatmapInfo.Ruleset; From de007cc1c63da3bd88ee52881a31f8cce91c2ec0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 24 Jul 2020 13:40:01 +0900 Subject: [PATCH 59/78] Use IEnumerable mods instead of IReadOnlyList --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index 12d472e8c6..914874e210 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -58,7 +58,7 @@ namespace osu.Game.Beatmaps /// The s to get the difficulty with. /// An optional which stops updating the star difficulty for the given . /// A bindable that is updated to contain the star difficulty when it becomes available. - public IBindable GetUntrackedBindable([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null, + public IBindable GetUntrackedBindable([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IEnumerable mods = null, CancellationToken cancellationToken = default) => createBindable(beatmapInfo, rulesetInfo, mods, cancellationToken); @@ -86,7 +86,7 @@ namespace osu.Game.Beatmaps /// The s to get the difficulty with. /// An optional which stops computing the star difficulty. /// The . - public async Task GetDifficultyAsync([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null, + public async Task GetDifficultyAsync([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IEnumerable mods = null, CancellationToken cancellationToken = default) { if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) @@ -103,7 +103,7 @@ namespace osu.Game.Beatmaps /// The to get the difficulty with. /// The s to get the difficulty with. /// The . - public StarDifficulty GetDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null) + public StarDifficulty GetDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IEnumerable mods = null) { if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) return existing; @@ -138,7 +138,7 @@ namespace osu.Game.Beatmaps /// The to update with. /// The s to update with. /// A token that may be used to cancel this update. - private void updateBindable([NotNull] BindableStarDifficulty bindable, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IReadOnlyList mods, CancellationToken cancellationToken = default) + private void updateBindable([NotNull] BindableStarDifficulty bindable, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable mods, CancellationToken cancellationToken = default) { GetDifficultyAsync(bindable.Beatmap, rulesetInfo, mods, cancellationToken).ContinueWith(t => { @@ -159,7 +159,7 @@ namespace osu.Game.Beatmaps /// The initial s to get the difficulty with. /// An optional which stops updating the star difficulty for the given . /// The . - private BindableStarDifficulty createBindable([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo initialRulesetInfo, [CanBeNull] IReadOnlyList initialMods, + private BindableStarDifficulty createBindable([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo initialRulesetInfo, [CanBeNull] IEnumerable initialMods, CancellationToken cancellationToken) { var bindable = new BindableStarDifficulty(beatmapInfo, cancellationToken); @@ -204,7 +204,7 @@ namespace osu.Game.Beatmaps /// The existing difficulty value, if present. /// The key that was used to perform this lookup. This can be further used to query . /// Whether an existing difficulty was found. - private bool tryGetExisting(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IReadOnlyList mods, out StarDifficulty existingDifficulty, out DifficultyCacheLookup key) + private bool tryGetExisting(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IEnumerable mods, out StarDifficulty existingDifficulty, out DifficultyCacheLookup key) { // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. rulesetInfo ??= beatmapInfo.Ruleset; From b10b99a6703c9a68d2f1da1b013a46460548a988 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 24 Jul 2020 13:52:43 +0900 Subject: [PATCH 60/78] Change method signatures to remove tracked/untracked --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 37 +++++++++---------- .../Carousel/DrawableCarouselBeatmap.cs | 2 +- .../Screens/Select/Details/AdvancedStats.cs | 4 +- 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index 914874e210..d86c0dd945 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -47,6 +47,19 @@ namespace osu.Game.Beatmaps currentMods.BindValueChanged(_ => updateTrackedBindables(), true); } + /// + /// Retrieves a bindable containing the star difficulty of a that follows the currently-selected ruleset and mods. + /// + /// The to get the difficulty of. + /// An optional which stops updating the star difficulty for the given . + /// A bindable that is updated to contain the star difficulty when it becomes available. + public IBindable GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, CancellationToken cancellationToken = default) + { + var bindable = createBindable(beatmapInfo, currentRuleset.Value, currentMods.Value, cancellationToken); + trackedBindables.Add(bindable); + return bindable; + } + /// /// Retrieves a bindable containing the star difficulty of a with a given and combination. /// @@ -54,30 +67,14 @@ namespace osu.Game.Beatmaps /// The bindable will not update to follow the currently-selected ruleset and mods. /// /// The to get the difficulty of. - /// The to get the difficulty with. - /// The s to get the difficulty with. + /// The to get the difficulty with. If null, the difficulty will change along with the game-wide ruleset and mods. + /// The s to get the difficulty with. If null, the difficulty will change along with the game-wide mods. /// An optional which stops updating the star difficulty for the given . /// A bindable that is updated to contain the star difficulty when it becomes available. - public IBindable GetUntrackedBindable([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IEnumerable mods = null, - CancellationToken cancellationToken = default) + public IBindable GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, [NotNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable mods, + CancellationToken cancellationToken = default) => createBindable(beatmapInfo, rulesetInfo, mods, cancellationToken); - /// - /// Retrieves a bindable containing the star difficulty of a that follows the user's currently-selected ruleset and mods. - /// - /// - /// Ensure to hold a local reference of the returned bindable in order to receive value-changed events. - /// - /// The to get the difficulty of. - /// An optional which stops updating the star difficulty for the given . - /// A bindable that is updated to contain the star difficulty when it becomes available, or when the currently-selected ruleset and mods change. - public IBindable GetTrackedBindable([NotNull] BeatmapInfo beatmapInfo, CancellationToken cancellationToken = default) - { - var bindable = createBindable(beatmapInfo, currentRuleset.Value, currentMods.Value, cancellationToken); - trackedBindables.Add(bindable); - return bindable; - } - /// /// Retrieves the difficulty of a . /// diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 1b5b448e1f..c559b4f8f5 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -194,7 +194,7 @@ namespace osu.Game.Screens.Select.Carousel if (Item.State.Value != CarouselItemState.Collapsed) { // We've potentially cancelled the computation above so a new bindable is required. - starDifficultyBindable = difficultyManager.GetTrackedBindable(beatmap, (starDifficultyCancellationSource = new CancellationTokenSource()).Token); + starDifficultyBindable = difficultyManager.GetBindableDifficulty(beatmap, (starDifficultyCancellationSource = new CancellationTokenSource()).Token); starDifficultyBindable.BindValueChanged(d => starCounter.Current = (float)d.NewValue.Stars, true); } diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index aefba397b9..1557a025ef 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -161,8 +161,8 @@ namespace osu.Game.Screens.Select.Details var ourSource = starDifficultyCancellationSource = new CancellationTokenSource(); - normalStarDifficulty = difficultyManager.GetUntrackedBindable(Beatmap, ruleset.Value, cancellationToken: ourSource.Token); - moddedStarDifficulty = difficultyManager.GetUntrackedBindable(Beatmap, ruleset.Value, mods.Value, ourSource.Token); + normalStarDifficulty = difficultyManager.GetBindableDifficulty(Beatmap, ruleset.Value, null, cancellationToken: ourSource.Token); + moddedStarDifficulty = difficultyManager.GetBindableDifficulty(Beatmap, ruleset.Value, mods.Value, ourSource.Token); normalStarDifficulty.BindValueChanged(_ => updateDisplay()); moddedStarDifficulty.BindValueChanged(_ => updateDisplay(), true); From 44b0aae20d753f707065d21cf9d25da7b52936c3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 24 Jul 2020 13:54:47 +0900 Subject: [PATCH 61/78] Allow nullable ruleset, reword xmldoc --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index d86c0dd945..5e644fbf1c 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -67,11 +67,11 @@ namespace osu.Game.Beatmaps /// The bindable will not update to follow the currently-selected ruleset and mods. /// /// The to get the difficulty of. - /// The to get the difficulty with. If null, the difficulty will change along with the game-wide ruleset and mods. - /// The s to get the difficulty with. If null, the difficulty will change along with the game-wide mods. + /// The to get the difficulty with. If null, the 's ruleset is used. + /// The s to get the difficulty with. If null, no mods will be assumed. /// An optional which stops updating the star difficulty for the given . /// A bindable that is updated to contain the star difficulty when it becomes available. - public IBindable GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, [NotNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable mods, + public IBindable GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable mods, CancellationToken cancellationToken = default) => createBindable(beatmapInfo, rulesetInfo, mods, cancellationToken); From d093dc09f94a194eb8e1d936c7ccf36b3a1ff712 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jul 2020 14:10:05 +0900 Subject: [PATCH 62/78] Limit notification text length to avoid large error messages degrading performance --- osu.Game/OsuGame.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index f4bb10340e..d6a07651e2 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -18,6 +18,7 @@ using osu.Game.Screens.Menu; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Humanizer; using JetBrains.Annotations; using osu.Framework.Audio; using osu.Framework.Bindables; @@ -759,7 +760,7 @@ namespace osu.Game Schedule(() => notifications.Post(new SimpleNotification { Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb, - Text = entry.Message + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty), + Text = entry.Message.Truncate(256) + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty), })); } else if (recentLogCount == short_term_display_limit) From 4e0f16a45059996dd0ac01ef70cf6bace62b70a7 Mon Sep 17 00:00:00 2001 From: Poliwrath Date: Fri, 24 Jul 2020 02:00:18 -0400 Subject: [PATCH 63/78] Add JPEG screenshot quality setting --- osu.Game/Configuration/OsuConfigManager.cs | 2 ++ osu.Game/Graphics/ScreenshotManager.cs | 5 ++++- .../Overlays/Settings/Sections/Graphics/DetailSettings.cs | 5 +++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 268328272c..a45f5994b7 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -106,6 +106,7 @@ namespace osu.Game.Configuration Set(OsuSetting.Version, string.Empty); Set(OsuSetting.ScreenshotFormat, ScreenshotFormat.Jpg); + Set(OsuSetting.ScreenshotJpegQuality, 75, 0, 100); Set(OsuSetting.ScreenshotCaptureMenuCursor, false); Set(OsuSetting.SongSelectRightMouseScroll, false); @@ -212,6 +213,7 @@ namespace osu.Game.Configuration ShowConvertedBeatmaps, Skin, ScreenshotFormat, + ScreenshotJpegQuality, ScreenshotCaptureMenuCursor, SongSelectRightMouseScroll, BeatmapSkins, diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index 9804aefce8..091e206a80 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -19,6 +19,7 @@ using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; namespace osu.Game.Graphics { @@ -33,6 +34,7 @@ namespace osu.Game.Graphics public IBindable CursorVisibility => cursorVisibility; private Bindable screenshotFormat; + private Bindable screenshotJpegQuality; private Bindable captureMenuCursor; [Resolved] @@ -51,6 +53,7 @@ namespace osu.Game.Graphics this.storage = storage.GetStorageForDirectory(@"screenshots"); screenshotFormat = config.GetBindable(OsuSetting.ScreenshotFormat); + screenshotJpegQuality = config.GetBindable(OsuSetting.ScreenshotJpegQuality); captureMenuCursor = config.GetBindable(OsuSetting.ScreenshotCaptureMenuCursor); shutter = audio.Samples.Get("UI/shutter"); @@ -119,7 +122,7 @@ namespace osu.Game.Graphics break; case ScreenshotFormat.Jpg: - image.SaveAsJpeg(stream); + image.SaveAsJpeg(stream, new JpegEncoder { Quality = screenshotJpegQuality.Value }); break; default: diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs index 3089040f96..8b783fb104 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs @@ -31,6 +31,11 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics LabelText = "Screenshot format", Bindable = config.GetBindable(OsuSetting.ScreenshotFormat) }, + new SettingsSlider + { + LabelText = "JPEG Screenshot quality", + Bindable = config.GetBindable(OsuSetting.ScreenshotJpegQuality) + }, new SettingsCheckbox { LabelText = "Show menu cursor in screenshots", From 05235c70c53186c5b3bceca9d8ad3963463ee9ec Mon Sep 17 00:00:00 2001 From: Poliwrath Date: Fri, 24 Jul 2020 02:26:45 -0400 Subject: [PATCH 64/78] remove jpeg quality setting, use 92 for quality --- osu.Game/Configuration/OsuConfigManager.cs | 2 -- osu.Game/Graphics/ScreenshotManager.cs | 6 +++--- .../Overlays/Settings/Sections/Graphics/DetailSettings.cs | 5 ----- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index a45f5994b7..268328272c 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -106,7 +106,6 @@ namespace osu.Game.Configuration Set(OsuSetting.Version, string.Empty); Set(OsuSetting.ScreenshotFormat, ScreenshotFormat.Jpg); - Set(OsuSetting.ScreenshotJpegQuality, 75, 0, 100); Set(OsuSetting.ScreenshotCaptureMenuCursor, false); Set(OsuSetting.SongSelectRightMouseScroll, false); @@ -213,7 +212,6 @@ namespace osu.Game.Configuration ShowConvertedBeatmaps, Skin, ScreenshotFormat, - ScreenshotJpegQuality, ScreenshotCaptureMenuCursor, SongSelectRightMouseScroll, BeatmapSkins, diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index 091e206a80..d1f6fd445e 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -34,7 +34,6 @@ namespace osu.Game.Graphics public IBindable CursorVisibility => cursorVisibility; private Bindable screenshotFormat; - private Bindable screenshotJpegQuality; private Bindable captureMenuCursor; [Resolved] @@ -53,7 +52,6 @@ namespace osu.Game.Graphics this.storage = storage.GetStorageForDirectory(@"screenshots"); screenshotFormat = config.GetBindable(OsuSetting.ScreenshotFormat); - screenshotJpegQuality = config.GetBindable(OsuSetting.ScreenshotJpegQuality); captureMenuCursor = config.GetBindable(OsuSetting.ScreenshotCaptureMenuCursor); shutter = audio.Samples.Get("UI/shutter"); @@ -122,7 +120,9 @@ namespace osu.Game.Graphics break; case ScreenshotFormat.Jpg: - image.SaveAsJpeg(stream, new JpegEncoder { Quality = screenshotJpegQuality.Value }); + const int jpeg_quality = 92; + + image.SaveAsJpeg(stream, new JpegEncoder { Quality = jpeg_quality }); break; default: diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs index 8b783fb104..3089040f96 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs @@ -31,11 +31,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics LabelText = "Screenshot format", Bindable = config.GetBindable(OsuSetting.ScreenshotFormat) }, - new SettingsSlider - { - LabelText = "JPEG Screenshot quality", - Bindable = config.GetBindable(OsuSetting.ScreenshotJpegQuality) - }, new SettingsCheckbox { LabelText = "Show menu cursor in screenshots", From 877b985e900a1c4669e04dcb16f47f760232aafd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 24 Jul 2020 16:11:28 +0900 Subject: [PATCH 65/78] Remove local cancellation token --- osu.Game/Screens/Select/Details/AdvancedStats.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index 1557a025ef..44c328187f 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -159,10 +159,10 @@ namespace osu.Game.Screens.Select.Details if (Beatmap == null) return; - var ourSource = starDifficultyCancellationSource = new CancellationTokenSource(); + starDifficultyCancellationSource = new CancellationTokenSource(); - normalStarDifficulty = difficultyManager.GetBindableDifficulty(Beatmap, ruleset.Value, null, cancellationToken: ourSource.Token); - moddedStarDifficulty = difficultyManager.GetBindableDifficulty(Beatmap, ruleset.Value, mods.Value, ourSource.Token); + normalStarDifficulty = difficultyManager.GetBindableDifficulty(Beatmap, ruleset.Value, null, starDifficultyCancellationSource.Token); + moddedStarDifficulty = difficultyManager.GetBindableDifficulty(Beatmap, ruleset.Value, mods.Value, starDifficultyCancellationSource.Token); normalStarDifficulty.BindValueChanged(_ => updateDisplay()); moddedStarDifficulty.BindValueChanged(_ => updateDisplay(), true); From dbe9180c55c4e4d6a8991b76aa48a9a4b5f46674 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jul 2020 16:38:48 +0900 Subject: [PATCH 66/78] Rename class and remove screen conditionals --- osu.Desktop/OsuGameDesktop.cs | 2 +- ...KeyHandler.cs => GameplayWinKeyBlocker.cs} | 23 +++++-------------- osu.Desktop/Windows/WindowsKey.cs | 2 +- 3 files changed, 8 insertions(+), 19 deletions(-) rename osu.Desktop/Windows/{GameplayWinKeyHandler.cs => GameplayWinKeyBlocker.cs} (55%) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 6eefee3b50..2079f136d2 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -101,7 +101,7 @@ namespace osu.Desktop LoadComponentAsync(new DiscordRichPresence(), Add); if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) - LoadComponentAsync(new GameplayWinKeyHandler(ScreenStack), Add); + LoadComponentAsync(new GameplayWinKeyBlocker(), Add); } protected override void ScreenChanged(IScreen lastScreen, IScreen newScreen) diff --git a/osu.Desktop/Windows/GameplayWinKeyHandler.cs b/osu.Desktop/Windows/GameplayWinKeyBlocker.cs similarity index 55% rename from osu.Desktop/Windows/GameplayWinKeyHandler.cs rename to osu.Desktop/Windows/GameplayWinKeyBlocker.cs index 96154356d0..86174ceb90 100644 --- a/osu.Desktop/Windows/GameplayWinKeyHandler.cs +++ b/osu.Desktop/Windows/GameplayWinKeyBlocker.cs @@ -1,49 +1,38 @@ // 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Game.Configuration; -using osu.Game.Screens; -using osu.Game.Screens.Play; namespace osu.Desktop.Windows { - public class GameplayWinKeyHandler : Component + public class GameplayWinKeyBlocker : Component { private Bindable allowScreenSuspension; private Bindable disableWinKey; - private readonly OsuScreenStack screenStack; private GameHost host; - private Type currentScreenType => screenStack.CurrentScreen?.GetType(); - - public GameplayWinKeyHandler(OsuScreenStack stack) - { - screenStack = stack; - } - [BackgroundDependencyLoader] private void load(GameHost host, OsuConfigManager config) { this.host = host; allowScreenSuspension = host.AllowScreenSuspension.GetBoundCopy(); - allowScreenSuspension.ValueChanged += toggleWinKey; + allowScreenSuspension.BindValueChanged(_ => updateBlocking()); disableWinKey = config.GetBindable(OsuSetting.GameplayDisableWinKey); - disableWinKey.BindValueChanged(t => allowScreenSuspension.TriggerChange(), true); + disableWinKey.BindValueChanged(_ => updateBlocking(), true); } - private void toggleWinKey(ValueChangedEvent e) + private void updateBlocking() { - var isPlayer = typeof(Player).IsAssignableFrom(currentScreenType) && currentScreenType != typeof(ReplayPlayer); + bool shouldDisable = disableWinKey.Value && !allowScreenSuspension.Value; - if (!e.NewValue && disableWinKey.Value && isPlayer) + if (shouldDisable) host.InputThread.Scheduler.Add(WindowsKey.Disable); else host.InputThread.Scheduler.Add(WindowsKey.Enable); diff --git a/osu.Desktop/Windows/WindowsKey.cs b/osu.Desktop/Windows/WindowsKey.cs index 4a815b135e..f19d741107 100644 --- a/osu.Desktop/Windows/WindowsKey.cs +++ b/osu.Desktop/Windows/WindowsKey.cs @@ -21,7 +21,7 @@ namespace osu.Desktop.Windows private static IntPtr keyHook; [StructLayout(LayoutKind.Explicit)] - private struct KdDllHookStruct + private readonly struct KdDllHookStruct { [FieldOffset(0)] public readonly int VkCode; From 5f98195144d1068063bfd015d2501255afcd0a34 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 24 Jul 2020 18:16:36 +0900 Subject: [PATCH 67/78] Load nested hitobjects during map load --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index f275153ce3..581617b567 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -129,9 +129,9 @@ namespace osu.Game.Rulesets.Objects.Drawables LoadSamples(); } - protected override void LoadComplete() + protected override void LoadAsyncComplete() { - base.LoadComplete(); + base.LoadAsyncComplete(); HitObject.DefaultsApplied += onDefaultsApplied; @@ -148,6 +148,11 @@ namespace osu.Game.Rulesets.Objects.Drawables samplesBindable.CollectionChanged += (_, __) => LoadSamples(); apply(HitObject); + } + + protected override void LoadComplete() + { + base.LoadComplete(); updateState(ArmedState.Idle, true); } From 2b068298cc339f3134662331037569cf08b18cd3 Mon Sep 17 00:00:00 2001 From: bastoo0 <37190278+bastoo0@users.noreply.github.com> Date: Fri, 24 Jul 2020 12:01:23 +0200 Subject: [PATCH 68/78] Fix inconsistency between this and osu-performance The bonus value for HD is given twice here (probably a merge issue). The correct bonus is currently used on stable: https://github.com/ppy/osu-performance/blob/736515a0347ba909d5ac303df7051b600f6655be/src/performance/catch/CatchScore.cpp#L68 --- osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs index 2ee7cea645..d700f79e5b 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs @@ -78,7 +78,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty if (mods.Any(m => m is ModHidden)) { - value *= 1.05 + 0.075 * (10.0 - Math.Min(10.0, Attributes.ApproachRate)); // 7.5% for each AR below 10 // Hiddens gives almost nothing on max approach rate, and more the lower it is if (approachRate <= 10.0) value *= 1.05 + 0.075 * (10.0 - approachRate); // 7.5% for each AR below 10 From 8f841b47e68ea251a6bd7ec832007d8e50220d11 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 24 Jul 2020 19:24:20 +0900 Subject: [PATCH 69/78] Cancel previous initial state computations --- .../Scrolling/ScrollingHitObjectContainer.cs | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 0dc3324559..bf64175468 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -2,11 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Layout; +using osu.Framework.Threading; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osuTK; @@ -17,7 +19,7 @@ namespace osu.Game.Rulesets.UI.Scrolling { private readonly IBindable timeRange = new BindableDouble(); private readonly IBindable direction = new Bindable(); - private readonly Dictionary hitObjectInitialStateCache = new Dictionary(); + private readonly Dictionary hitObjectInitialStateCache = new Dictionary(); [Resolved] private IScrollingInfo scrollingInfo { get; set; } @@ -175,10 +177,10 @@ namespace osu.Game.Rulesets.UI.Scrolling { // The cache may not exist if the hitobject state hasn't been computed yet (e.g. if the hitobject was added + defaults applied in the same frame). // In such a case, combinedObjCache will take care of updating the hitobject. - if (hitObjectInitialStateCache.TryGetValue(drawableObject, out var objCache)) + if (hitObjectInitialStateCache.TryGetValue(drawableObject, out var state)) { combinedObjCache.Invalidate(); - objCache.Invalidate(); + state.Cache.Invalidate(); } } @@ -190,8 +192,8 @@ namespace osu.Game.Rulesets.UI.Scrolling if (!layoutCache.IsValid) { - foreach (var cached in hitObjectInitialStateCache.Values) - cached.Invalidate(); + foreach (var state in hitObjectInitialStateCache.Values) + state.Cache.Invalidate(); combinedObjCache.Invalidate(); scrollingInfo.Algorithm.Reset(); @@ -215,16 +217,18 @@ namespace osu.Game.Rulesets.UI.Scrolling foreach (var obj in Objects) { - if (!hitObjectInitialStateCache.TryGetValue(obj, out var objCache)) - objCache = hitObjectInitialStateCache[obj] = new Cached(); + if (!hitObjectInitialStateCache.TryGetValue(obj, out var state)) + state = hitObjectInitialStateCache[obj] = new InitialState(new Cached()); - if (objCache.IsValid) + if (state.Cache.IsValid) continue; - computeLifetimeStartRecursive(obj); - computeInitialStateRecursive(obj); + state.ScheduledComputation?.Cancel(); + state.ScheduledComputation = computeInitialStateRecursive(obj); - objCache.Validate(); + computeLifetimeStartRecursive(obj); + + state.Cache.Validate(); } combinedObjCache.Validate(); @@ -267,8 +271,7 @@ namespace osu.Game.Rulesets.UI.Scrolling return scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, originAdjustment, timeRange.Value, scrollLength); } - // Cant use AddOnce() since the delegate is re-constructed every invocation - private void computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() => + private ScheduledDelegate computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() => { if (hitObject.HitObject is IHasDuration e) { @@ -325,5 +328,19 @@ namespace osu.Game.Rulesets.UI.Scrolling break; } } + + private class InitialState + { + [NotNull] + public readonly Cached Cache; + + [CanBeNull] + public ScheduledDelegate ScheduledComputation; + + public InitialState(Cached cache) + { + Cache = cache; + } + } } } From eb84f2503657fe5c8332e18c20ac9b0281d45d87 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jul 2020 19:34:13 +0900 Subject: [PATCH 70/78] Adjust maximum spins to roughly match stable --- osu.Game.Rulesets.Osu/Objects/Spinner.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 1c30058d5d..9699ab9502 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -26,7 +26,10 @@ namespace osu.Game.Rulesets.Osu.Objects /// public int SpinsRequired { get; protected set; } = 1; - public int MaximumBonusSpins => SpinsRequired; + /// + /// Number of spins available to give bonus, beyond . + /// + public int MaximumBonusSpins => (int)(SpinsRequired * 1.8f); // roughly matches stable protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { From 82e4050fddb27908c282a5f307f62b22ef62940c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jul 2020 19:41:34 +0900 Subject: [PATCH 71/78] Fix xmldoc --- .../Objects/Drawables/Pieces/SpinnerBonusDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs index 76d7f1843e..a8f5580735 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs @@ -9,7 +9,7 @@ using osu.Game.Graphics.Sprites; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { /// - /// A component that tracks spinner spins and add bonus score for it. + /// Shows incremental bonus score achieved for a spinner. /// public class SpinnerBonusDisplay : CompositeDrawable { From dd45f0bd40d7aad6def77ce95ed2d2013cd03082 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jul 2020 21:03:55 +0900 Subject: [PATCH 72/78] Adjust max spins to "match" stable --- osu.Game.Rulesets.Osu/Objects/Spinner.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 9699ab9502..2c03e6eeac 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -29,16 +29,21 @@ namespace osu.Game.Rulesets.Osu.Objects /// /// Number of spins available to give bonus, beyond . /// - public int MaximumBonusSpins => (int)(SpinsRequired * 1.8f); // roughly matches stable + public int MaximumBonusSpins { get; protected set; } = 1; protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - SpinsRequired = (int)(Duration / 1000 * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5)); + double secondsDuration = Duration / 1000; // spinning doesn't match 1:1 with stable, so let's fudge them easier for the time being. - SpinsRequired = (int)Math.Max(1, SpinsRequired * 0.6); + double minimumRotationsPerSecond = 0.6 * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5); + + const double maximum_rotations_per_second = 8; // close to 477rpm. + + SpinsRequired = (int)Math.Max(1, (secondsDuration * minimumRotationsPerSecond)); + MaximumBonusSpins = (int)(maximum_rotations_per_second / minimumRotationsPerSecond * secondsDuration); } protected override void CreateNestedHitObjects() From a6a7961af9c2788202055efff6a9e42cfd3a7344 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jul 2020 22:09:25 +0900 Subject: [PATCH 73/78] Change div to subtraction to fix calculation --- osu.Game.Rulesets.Osu/Objects/Spinner.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 2c03e6eeac..619b49926e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -35,15 +35,18 @@ namespace osu.Game.Rulesets.Osu.Objects { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); + // spinning doesn't match 1:1 with stable, so let's fudge them easier for the time being. + const double stable_matching_fudge = 0.6; + + // close to 477rpm + const double maximum_rotations_per_second = 8; + double secondsDuration = Duration / 1000; - // spinning doesn't match 1:1 with stable, so let's fudge them easier for the time being. - double minimumRotationsPerSecond = 0.6 * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5); - - const double maximum_rotations_per_second = 8; // close to 477rpm. + double minimumRotationsPerSecond = stable_matching_fudge * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5); SpinsRequired = (int)Math.Max(1, (secondsDuration * minimumRotationsPerSecond)); - MaximumBonusSpins = (int)(maximum_rotations_per_second / minimumRotationsPerSecond * secondsDuration); + MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration); } protected override void CreateNestedHitObjects() From 897ab4a9bb2de4b5ffd2cbb9510cd74c758cfc20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 25 Jul 2020 11:53:38 +0200 Subject: [PATCH 74/78] Add example test resources to demonstrate fail case --- .../Resources/special-skin/pippidonclear.png | Bin 0 -> 8462 bytes .../Resources/special-skin/pippidonfail.png | Bin 0 -> 10643 bytes .../Resources/special-skin/pippidonidle.png | Bin 0 -> 10090 bytes .../Resources/special-skin/pippidonkiai.png | Bin 0 -> 12368 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonclear.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonfail.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonidle.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonkiai.png diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonclear.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonclear.png new file mode 100644 index 0000000000000000000000000000000000000000..c5bcdbd3fc13b3ca847cfba5402c2db38cd962c7 GIT binary patch literal 8462 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonfail.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonfail.png new file mode 100644 index 0000000000000000000000000000000000000000..39cf7372851ff962b02d9184fd9169faf06c702b GIT binary patch literal 10643 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 b?-&p%=c-}*c9uXxB}hdQfLfd!P#rCUH62?6Pr zu6Oyp_s9EVKlkoA_nb5HojG&n%uM8KO{GU5S`Zc%)*}^Vc^xb)Z1CNW01s&K3ykpw z9uM4t%h%VJ*WSg^69#p&=5_V3&DfQs z#lm`ur6T`I@9pQE-+piPR;RlU_5v#tLId?*DdTX}>KyC4WB)QJaxGP6vnzF^FmW{b z!8=}!oLMa$clcdzM>Xf5@v@X~_XFYXMCr>HbQw-iGTb*Abqg}2@hWVyZ)~yz1y9%N z-zAHQc@D*f=Q}mxprnS%%_#!D1p*c#mD` zI*fN{a@+XG&guGy@}7b{&gSdQ$HD|oudP24(8={Fk_f*;OOsm}or@M}Y%k26W!4f| zlDhWc3_cN0XzG9kC{wdAbv;F3gF^TNv_;FDago?BIQoCVXjC;6@zf^AUst#l3rP&w zk`92lI|E^LL{1Ji7K@gl4>&Gy$U-E$N8Xh@`1Tm`5d7+G;q>xOcvlbYWSjaG1ezbm zNhsubmZko%jWvXYf(_fhb|~30`&*@QsG%I%ZW%OR6%W}&V^pm|l4q3C#qd@+^keSb z;NLzOI`0%-d_+NsU9L>YmlB&mUnZBf4uL*PqhWN6UvqCH=;r=R@XtGwvv(Vd@_}Sw zpXVxUm3}fr{nQA0ZfaR!<4Dxa^qH0CzW%njZd8%HpDMIjXg}b*d35U71i@_pktTmV z_$8f1k08QB{P{2M0-=igtNaHXlk}3#tE)0V|42ceCT>{WP|{!FTuEMrRb1hNUzy>` ziCZtbK>c+uf9#er4N)zDQF&Jm^cbnE!xa4JRoEnSG>rm@JH{o%F7q)_A?^N?@M~XX zw!JTNM%mqgu{*r8X5lI#C(#KnLqM5dXiPc!L_%Jb9i^I2-<#XA-e1G-?$H6}fKwRY z+$;XavBUVJ#lreN=#IRAlG+`kNeIf&G-nf%x=*N(CS2(&CS1c>BF?G=S3YLZp9(flJnzP zQN~(Y>Rs{J18it6M~yAKcH_pqa@_l~o+W%-Eu=GDgJ|vP0G^L$X7|}BUI^Wb43iz{ zuK5Z~)F`Uvh2s&{kU}l;Uj|8=ylqpfh95)Clvo)cCxx$h#<)3GNM!FWVxx#Qgu5@w zI&14`Zx^Tp-$rj;et7w`mA|YimHbPiGG0m&RiA%ZE-N87$|bc~58rmht(0TozJTc( zKj*VhkKlrC3S#nJ@k!{_5U7N#bghW@)!Boe!`=*uqtf@wO3d0yh2s;Eg$dJ`Ok+>3c5h~fopKgooKTW=ywFBfx zl|TLt!R3qXUpHj+@byPGs9(I~mUx5(1d1KPwL^TxAWCwbvp~ zT0RRp{@Z^Yafz-Q{nS$Wcxj-L*{|38amr{lx&Zuh0P2582G6!!D^~X)C~|AqUX1d} zbd}~I)zv;WEWn5gPV>FV9^%dPH!DZD7FWN59!75uz&9 z?HwntmO(P{9W=PXJy(V~FRP*^6YOhRaf8+m;KWnHy^Zr@Kp@YGG#L#p()mu9;yHDZ zJN>hHotrpxo^0Vgcuhs6K#^*iazu@4@iBK>J6qnP-YFD@#Wq4Yl2)Rw>N zt4MlsA|nI$mKlCzei9ezWqyo8Dlx({vy*x)bTiIvv{ClFe2b$ZL7=<-S+e+pr57j^ z!u~6{9~0$$2d~Pz9#XizPQGo#KF;$FZ%SBz&10r4-qO)AU_y8vAL?atkkTT<)9$-2 zE~TZL1g@qI@3?u@O$=A*Xvuq-xRqdH80pJgr-8R{k4EG7uSDZVYegA%JYoNJu`D90%XX+rK4&TE*_cfsX}hveidgI ziM|$8249T%6zc}PiD08W#Qo8oHMsW{C=FDSvzXlYq=)R(K|1=){JyBGG4D9#03*C- z97|pddnXN_=c4uC zio37GFy7=a^;9{}>LcG4(?@&J0P0H)+aV-fX9)!P&-8C<-Ddq9k$M~MI9j+gfL_AH z$w_0&kraJnymF#Efb@1BG=9u{Bf>`G#QagtR2n)n&9o%;BAsxcy{|9z4S&Q_;`;^; zjE2~sDGbaaB;L!J?krG~C4aPA94_t55(zbB=>6jFGW!hA*+?!0 zyI6nqq11`4y6vW)?9p3y`Qw)c#U|_5=+ApfI9j}OSlepuymVU}7ZF!an&H}QPWI-L zsXFp*sUQ;zG&z{po+B*zu2aY*dQKo@6nHU$k22ct7@B;PAtI`t zcp+_e5j1t$a4=R&Os*5tS{LGQZ3~zoy5Z}NI{yS z>k?}z4R(owu*thU8M~+#sK?Ns9GB}p)>2Fia7mG$y=ts>MI11~`*D)rxiwZZreM-5 zUkb`$vMbc65h@~S1i_ys@8`R7qQ2(Vl3QCzRy!jOns93#uKaoHB>ETrqDd#5=}L5B z_C*&fF?_nvaBCmpD)JYe%y~t-&bYNh3rkRKVYBu@iO`N=K)=>swTe~D74Wl(JurP< zWlW4%6^2P*K!^U@@(uh23W~~gVn{lK5j5GrkrT55aUP~lBrO1xr>J5Eq7HI2hX4KM z5eTlfMyT*G&pn2qrYoil)IlY#cxx~b5jq&Nc9c-KC#<5#oN5A4p_0B>E0Qki#PoY* zpE}2U-kCvWWlirPws(9G{G94jW^|%0H5nWF&>!<@f!3~a)G(cFg2@2qAAgT+=$g1t z7tR615$EHmuGA!<94#rTV;fv;stFp38RDhG`RzICM03450W^d#CouRIe0=4AKB;Y_ zd4W0+8J5tGvRinf4;_4GQbpcOx1;%q-+pJ*t}r{Y7%lv0(aTa#XzxW=UQ>JqK54|? zPudhnf)vFgX&A!Z`&qG6(1ctpparV2_(Yo2>l@zH5UM2;=BNQ*(vxij)p(Vl zZ45TdTSv6n1-epWG%~gGm+PynYCzmm0`--NJ6)-7;~v(Fu-K=-Us#9OM7bI43DUsS z-+ZBd5iO)cyP}vHAZa_DsfD5O1?pIUWIT9Qi&%*U953FL69jZ^gg9y zH3xgQbUc5T*ZQxaEYjals0wd4F77gEFR&mThaWv7r4IzcbmkCnNDWi(7V7vO(>!rN zK&Xk(dICdi=!O3#t3Vs?(Su`5?*32B3vvrt-O7W(`gCSAu(=2C;%$XvOd;mo-Em#i zWUlm?2@ZyLh7o_~B8veaHy2i@Z>Kp6-vOxs{%MbzGAqysmRVT-R!uldfy8sp0VL~h z9;zZYo85jWD!^oq;4dIkz2pUPqX0h1R^vJp0BDKm=Y3AX&=}fgcCwHUp^89L4+t;z z&8-<#D22gu%;N!SnCXE4MuHQC?f~^uFSF!il2f|L;{Y~Qy>KS$?Sc^`gq9wAJAPlj zi+Zd=I|M|3aUPus&+o%_x#OIDC7Nt+o;Mhc5$HOL>7)AneM<~=&4O}7@R$8ygceaC z`MbjNoS{F=m!#*-046qm24%~1&Erx_CNzbZ7RCcwQYmfj-T$zM;bmC2w9C;QoFBd- zSz3{v5ZSL|Lcp>Q4ybWU;S{4tsxInfUHDI9$cMXmyWgacP#QqGsAa~?6d|p4;o1NO z+&ur;4`|{E4eYAa)iR&xu(DIpuq&1I=gY5Dp-W|+zygNkVSZTxQ{ImOdZO0&rwIKX zy3==1!|F+O!lnrNsT0X1q3A*@xHaNH2DtrvqbH?>U1?cHgx|d+yvJA)e{I=%3Q+b4 zS2F3zBqy*gork0(HyEa_(}R4OTy{^fUel`iOPh@l^_5k~+LENsxdcTY*4nU!I3c+u75c2-v4nJxffipMvS_kf#(M=lxz*be%0R3qdfP ziO3iSPH=vgH6=g^;=4~0{?n}g5LKJD4h5vTEC)jzJOkd7Nitz4 zjprk(ac5tgXImn;?08Tf@lU5XN?4A6I2HvSJ^S$JxGXo7oXqK4;mKgiw{o*B@u+N` zEFRJie7ufWXRX@Q7A8lS<$-L|9?Tf~(SRZG7iiUQZWpUu=n-Dl`6f14GEm@%ovQLL zyxUCa@Yx7oz5OKS;^!;M1rv{MKHnJBCQ=_AF#kJ-EKRQhc>dxk!7^4fR@*(xUzTtF zQI+9CiJT(f&iPwk3N7CfL3t*1Da_f6_p5cog>Dhc@f3nyDtWd%kPh#};zg*D`|vW*MXoEte@R@q=d z&Rgdzk2(&@*3hr5wf`KQnbL`3Pa8$_TZUu~E+7*d7L<>VLF|Ei5H0cGvgE07IK>C z^A9+k7l3a|UcP;Rj{aoqmuB>D!T%O*Z2&2#5e}xl5&RW=74kOCC}#9*w7Q-k#pnk& z3b>yVH>SoZt5_`t;fFW5T$bnZOO2l}HdV{O?6v%Edfi8V`nc!G$?EwD+(afQ%`z4^ zmmn*g^EE!Y=IGWmChRyGgz;2i9OT||jsEF@kGJ)0I85a&&+wRB9*z+ur0SthI!{{6 z+ljIh&YntBf=Cajz*f*fZY1c7Kv`sh^369{kYX&r(}@CIX!`p1>+Ukwk@=NaE{~+H zlp6_GzR0uBkrD9w?`Jq14O!T-sZZWrm+(DyaPp0CRxbp!{8w}HQ>W>OTrIbxuI$j5 zhQfA?PAX%zXs>+-{}Oz>u_96$d zt-P?gFoKr9m>THoW&kK=%~&N~nBBc;8yOq+O6n>M#h1ABD1_@bB&=a&U=@$Y$;CLF z+G}=xHu-5;l`GF?WQR_R@^>LLF?jwCEY~d5tIcdt{2QHf6g?rqgM#bk8zn1QWpGAD zz7{&VLMzM~@W94(iN!!~5<2_NSDjo1Z}ms)soqEA$~Ozucmt=Pw=o1OYYMtl7S(*< zEN}$EAOpQ7AKf8va9}19)-*J*r#50=^#sta@}u@Pq<%d*=sqE~MAaNu!|h8D?i!qD z_G)Ps71&!AAQE3{B$_=KeM?`BTK`VivnkT5&CIA{AXu=KW>pWqBadb&YbsCm(eh9O zd2i1{lasb%!x{Cw0{TaI=ld5W7^I3K+w#zPNUr=%-t)qpH-Yh9Mi4CXIGGx!v6uaM zLAhGzHdB>ax0l%5H{RK~@?GUfnE>MKpsQ@OHO%I-b5(ShZb*x-ZKq520ioWeY-sO@ ztt!v#Bx$KLpd}`A@qc4$S{jV1`bHq4i zd&WSNcs_Kr+UXJCYp^{*K==~gkTab4?$C#>v$m>*vD)c!lLB0~Bd!#QSO7P#+Cr`G zqbL1RjE#PL?X#$FaH&y(03oNsRv5)|)?z)*Z}wZ1f3@hJw4xJydBnNUY;412pV?*tw0lFU;_xxxqF9vgFWlhMFk8NHgw-c&s)D| zHS;Aqn;5kVTKtxAnt6O68B=$e7EzU){!9O|E4uORUw6CxXsvZYkIVh!bH?u(_eS^M z`c(v!vb(O20EjL5N(E{a_cQAEgznvfozlW&8~x6Wgq$i(_{S8k!@L`VdN=N1+j}-? zWTf!rYnHD{ETN-Ig=Fl$i`GKQ=E=nbF-_2~ly>9}q$XX2_tK_;(K7L!(9}RiN?_u} z@siek#-M+OQ(Gn}Vydf-djs_Bg#H%=3{;fwTQx=+`H~~o+@~~7M{Lh_q5YFhR}XKS zPb6Y=(&YX2s8Y}_Bc9NHkMM_U!7jCQ2RRon)a;ArrjCtw6pk~Wpd;}^C`KAZl2;!x zbU{UKaeI3ROw6x`8@{D_M(66W%}d|(?|o}LjX_)g@q6NPbiG1Ezr>MT&a;3{s2geI zFUHg#J+_yvlS)4=sQ((SVbh`(JmPYG*>b~Fl3n${RCkblSaJY=(qh`>{KoJqJ8*Fn z8@}NC;*+{k4r7V%zLJwiOT@|f&^UQ!5Ohl55uV@9ADs#CTZ}drnd6puP?!ksxne`c zJR$B{#VP9KnGM#|ZJ+(6G?aQ)`;Uy*@UQ(q&iTw!PNOEpUHbXs6M)|Q2T8!(=G zi&CmPhpCS)QAK)L!Jbg&|G(E z5|}Wc7TR)**mW-z3kBO~8BK0y2*K&z9u1Y7Et5e{&exRoD=X=r9W}i;4m{I!75-U* zsrL9H1sd9xJv)Xq^qKAmrKq#CrdFv#r|SY;%fF&nkQpiqP-L0u`p74@@hks+FHO?AiCcelhd&+GZ(-efhC3VRyg}cxl1$jFI5xpUl#n(gH151Q9&IDv znAWsV8<=+3DhpVT)u%=}MNew;{RN|Zp4kjj12HoiRWGWK`H2OkCJ?WsNx*l@_p#un zmO`oo2SnWX@9jNJv)`&JzCGMjD6IV9Vc$^xNXkh->1Kn(KTcl6E<*QjOk%2OD0P)T z9uU%H*mpuzrxaISJX%~A)2ixzP*{Cw{QFSVoFlE{FnrHD4!HhJ&$S$FPZGyK3|I=2 z-3C4vmZIv{;E-gB4m27|atYMB`)@~9QIl6`&YEPcT@4L<0c=DSxW1}r3 z?9-Ot@$8u2R`C(RuH8`xl3=;zl8mV3_*LWf6A|o0wa2xrKw#kt&;k*PS;OGaS@;h!#H9Y@rX2Ly`Q?QUSz{76FxG>=cn}72T17Q@4i}v-zNkqsc=nF`0P@mH!`} zRKw5KzfO7PdzGNCl| zOjZ87c4^`BT_*@*5Qam;wV?YQEbAgJI0^AI-Fd&M99;fX_2oMSoW7W7X^xu_q13Tx zsGi+pe7^KFN;9K#eZzJdp6$-Oe^hCemf3p!Y$)k|x|o*`-`6McJ=X;pJZkgS({o~| zxBnQ0oOY)(Nod6C{>?hd;O$DpW0SFs*0}Kkk+lJY0*Ywg+u zpcKsU_Hc^anLE2>H9nyLhgB;pC>0RG7x(_IZ#Apd)`>OUc85w72@-Ea#c;#pq}&MV zJ?A-H-5Sg)@E7ZTk0q8+qQ$YZ!5bWjM4D>rJJS4QF}9;jW${pDX(Z|nmW8TF0w6o-rd!)!k&1Lx5r9mktk)yRZpg# zhhVb*Qug}-k$fYlM#<05II$@qa%R@gEr3`-KQe!>!up^=Vwh>X^qcBgJAcPf52Jp^ z`#^R9w*@|aOU$c>sn6r*nA*wG+r8F5)jv*z9lVUR6K%S<`mRb(K|YTiRzCn`CVV0M zx+W*xnY-9nGlH+&3UIQ8oaTReFgWP;*L`zINhi1iSn%8GmElk;)5?x;)YV8Q`S$c- z%at}1uxu0P>mVYY5ciIQjkm)IkqM&$_IWw%GK1D_0$;= zT;z5>$Luc6S;z+>)aNYvlg*q|A7h1hP`KYZ$2|11<9saWnqQh~z87XE@+ zbIyIyVPfn1K{U`G>37Ldn6_xBe?i*PjWZ<^WivuTX0MU=}x!138Xm*Vz7gkKsV<|LxT`sTRztKFr&=m|{C z%)!B4nYn_^yx{jhuFan$Xag9*xm#apRX6V)QFgXj88&pU__&6{uyzQlUaY+|QKxW- zJ1JJrVtEBoo6@Y>|C{5RVC7V#L;m@TUCR5%^mV~OvBV~)DEAQUYP+;_#L1hvJ z>lne7XEEtFAfF=eUm6eOJHYeDzgN#7PMrlc9BJoQarJspsVtW|5kX#-Z33oZH!*r8 z5QFy7)+sWKTD;T}MjSj)s4EUY%`J*6e^01KV*xp)S&(lWB3A)Ms-#et=Y*Qu6jyd- z4TNIfEBB5q#EUhhs#Pv4HCN!lnEEP_02K+O?$^d zl+*6|?ox5=@0$R`yYP!2YjW`=#G2wqCMSvS9Rb2%&QNJp1F~hyN&B=+G0ORsAIQ1E z?+>*rITx1i=?D0C*YO|Z?Z0exLoKi^M8lKBSs#vhglwFK(7gvwx+7HHe`$^Fk2-S} zFo%uP7nwjxRZEUjCq|G`;!}B*k8lc8fHSDp5$E3Vv_4_HBE23q!#%_L3&-+Ca=tIm0vFgUXD+8~aN1!V^8@@cL)JfOy+Gu3>G z3**}#wIV(Fij;E)M7a2!mY(dPJD*$9e<^H@B*DIJ)%>lekx0t$Q0MEa*U0fQONT84 zu}0Yu^_a#h_!hp)-)~-EZsjqOi87mC>rnHE%l8!k1Y?n_T%XgpYNq75d6Xax>4qGx zSm7L3kNk;p4jAv>q|^U-@5RTBCNW4&aF8-KF0icjC;8+r`-6k3;NpkARQw4*=^#%+ zO0P{$>u`mqNmr%~h0EaJG8T7iL22NiZ2P@<7UDqGh= z!yEh-h4Q8E+C!n=St@?oUFHVY6W-lRRU1HL9SZ-PmjdqC8tMb zCu_c{b{Pj9cWfk>I-yj)l@|Rq-Y3q3}P=TXZ03;+r0PcoENC0>(rk=Xx+ITvib4)J* zwI67p_M_E`=V^*jEAe|NU;<4rRjtBST}gmNktaP{G6dS$>!k0h@`&9&!qKTE;6186NcPnLq(g2SB0wi$tI}M<>dRc4>w! ziM=B?;Myb0R8&9R3}3(i()_>WJI4K6LYApp>h@NGeanBer7g*{rr85;VDMGXs`&{N z&qpx>Id$BEySk1fD{0cV0<4l^c4UM=(as=C%v@Z;o zQ6A6T9>?v8oAABBN#@v#KvP2_L9WSEea8P+J@xgyLztn4`}2;sK;DHshn%@I_+%Km zYG6szClE^e1@9|c9{6`lMDa8;(;G;eb7$_Gb z;9oLGHCUl^u}RZ9uz6sErQ;gJZ3$vO?Zo_HZ# lb+vR*$^SLjecAt(n29rSl-gJS1gMk4Qc=*9FO#+W@IRs?%IE+9 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonkiai.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonkiai.png new file mode 100644 index 0000000000000000000000000000000000000000..7de00b5390494b7406e7595de865a8d6a659adcb GIT binary patch literal 12368 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 k73Wz0B#q`)9?)YNC#MoxTnmB!20&4eQ Date: Sat, 25 Jul 2020 12:03:54 +0200 Subject: [PATCH 75/78] Add support for indexless mascot texture lookups --- .../Skinning/TaikoLegacySkinTransformer.cs | 5 +---- osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs | 9 ++++++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index 23d675cfb0..f032c5f485 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -91,10 +91,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning return null; case TaikoSkinComponents.Mascot: - if (GetTexture("pippidonclear0") != null) - return new DrawableTaikoMascot(); - - return null; + return new DrawableTaikoMascot(); } return Source.GetDrawableComponent(component); diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs index 6f25a5f662..9c76aea54c 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs @@ -128,6 +128,13 @@ namespace osu.Game.Rulesets.Taiko.UI } private static Texture getAnimationFrame(ISkin skin, TaikoMascotAnimationState state, int frameIndex) - => skin.GetTexture($"pippidon{state.ToString().ToLower()}{frameIndex}"); + { + var texture = skin.GetTexture($"pippidon{state.ToString().ToLower()}{frameIndex}"); + + if (frameIndex == 0 && texture == null) + texture = skin.GetTexture($"pippidon{state.ToString().ToLower()}"); + + return texture; + } } } From f7a330becd68780ddbb91543350eee87b15d15a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 25 Jul 2020 12:13:19 +0200 Subject: [PATCH 76/78] Fix tests failing due to not checking state early enough --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index cb6a0decde..47d8a5c012 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -8,6 +8,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -36,6 +37,10 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning private TaikoScoreProcessor scoreProcessor; private IEnumerable mascots => this.ChildrenOfType(); + + private IEnumerable animatedMascots => + mascots.Where(mascot => mascot.ChildrenOfType().All(animation => animation.FrameCount > 0)); + private IEnumerable playfields => this.ChildrenOfType(); [SetUp] @@ -72,11 +77,11 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning AddStep("set clear state", () => mascots.ForEach(mascot => mascot.State.Value = TaikoMascotAnimationState.Clear)); AddStep("miss", () => mascots.ForEach(mascot => mascot.LastResult.Value = new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss })); - AddAssert("skins with animations remain in clear state", () => someMascotsIn(TaikoMascotAnimationState.Clear)); + AddAssert("skins with animations remain in clear state", () => animatedMascotsIn(TaikoMascotAnimationState.Clear)); AddUntilStep("state reverts to fail", () => allMascotsIn(TaikoMascotAnimationState.Fail)); AddStep("set clear state again", () => mascots.ForEach(mascot => mascot.State.Value = TaikoMascotAnimationState.Clear)); - AddAssert("skins with animations change to clear", () => someMascotsIn(TaikoMascotAnimationState.Clear)); + AddAssert("skins with animations change to clear", () => animatedMascotsIn(TaikoMascotAnimationState.Clear)); } [Test] @@ -186,10 +191,18 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning private void assertStateAfterResult(JudgementResult judgementResult, TaikoMascotAnimationState expectedState) { - AddStep($"{judgementResult.Type.ToString().ToLower()} result for {judgementResult.Judgement.GetType().Name.Humanize(LetterCasing.LowerCase)}", - () => applyNewResult(judgementResult)); + TaikoMascotAnimationState[] mascotStates = null; - AddAssert($"state is {expectedState.ToString().ToLower()}", () => allMascotsIn(expectedState)); + AddStep($"{judgementResult.Type.ToString().ToLower()} result for {judgementResult.Judgement.GetType().Name.Humanize(LetterCasing.LowerCase)}", + () => + { + applyNewResult(judgementResult); + // store the states as soon as possible, so that the delay between steps doesn't incorrectly fail the test + // due to not checking if the state changed quickly enough. + Schedule(() => mascotStates = animatedMascots.Select(mascot => mascot.State.Value).ToArray()); + }); + + AddAssert($"state is {expectedState.ToString().ToLower()}", () => mascotStates.All(state => state == expectedState)); } private void applyNewResult(JudgementResult judgementResult) @@ -211,6 +224,6 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning } private bool allMascotsIn(TaikoMascotAnimationState state) => mascots.All(d => d.State.Value == state); - private bool someMascotsIn(TaikoMascotAnimationState state) => mascots.Any(d => d.State.Value == state); + private bool animatedMascotsIn(TaikoMascotAnimationState state) => animatedMascots.Any(d => d.State.Value == state); } } From 648f9204f5c59a992080f7bd9d9777a4852ce7ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 26 Jul 2020 15:09:12 +0200 Subject: [PATCH 77/78] Add sample lifetime constraints for taiko --- .../Audio/DrumSampleContainer.cs | 64 +++++++++++++++++++ .../Audio/DrumSampleMapping.cs | 52 --------------- .../Skinning/LegacyInputDrum.cs | 4 +- osu.Game.Rulesets.Taiko/UI/InputDrum.cs | 10 +-- osu.Game/Skinning/SkinnableSound.cs | 3 + 5 files changed, 74 insertions(+), 59 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs delete mode 100644 osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs new file mode 100644 index 0000000000..7c39c040b1 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs @@ -0,0 +1,64 @@ +// 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.Framework.Graphics.Containers; +using osu.Game.Audio; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Taiko.Audio +{ + /// + /// Stores samples for the input drum. + /// The lifetime of the samples is adjusted so that they are only alive during the appropriate sample control point. + /// + public class DrumSampleContainer : LifetimeManagementContainer + { + private readonly ControlPointInfo controlPoints; + private readonly Dictionary mappings = new Dictionary(); + + public DrumSampleContainer(ControlPointInfo controlPoints) + { + this.controlPoints = controlPoints; + + IReadOnlyList samplePoints = controlPoints.SamplePoints.Count == 0 ? new[] { controlPoints.SamplePointAt(double.MinValue) } : controlPoints.SamplePoints; + + for (int i = 0; i < samplePoints.Count; i++) + { + var samplePoint = samplePoints[i]; + + var centre = samplePoint.GetSampleInfo(); + var rim = samplePoint.GetSampleInfo(HitSampleInfo.HIT_CLAP); + + var lifetimeStart = i > 0 ? samplePoint.Time : double.MinValue; + var lifetimeEnd = i + 1 < samplePoints.Count ? samplePoints[i + 1].Time : double.MaxValue; + + mappings[samplePoint.Time] = new DrumSample + { + Centre = addSound(centre, lifetimeStart, lifetimeEnd), + Rim = addSound(rim, lifetimeStart, lifetimeEnd) + }; + } + } + + private SkinnableSound addSound(HitSampleInfo hitSampleInfo, double lifetimeStart, double lifetimeEnd) + { + var drawable = new SkinnableSound(hitSampleInfo) + { + LifetimeStart = lifetimeStart, + LifetimeEnd = lifetimeEnd + }; + AddInternal(drawable); + return drawable; + } + + public DrumSample SampleAt(double time) => mappings[controlPoints.SamplePointAt(time).Time]; + + public class DrumSample + { + public SkinnableSound Centre; + public SkinnableSound Rim; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs deleted file mode 100644 index c31b07344d..0000000000 --- a/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs +++ /dev/null @@ -1,52 +0,0 @@ -// 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.Audio; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Skinning; - -namespace osu.Game.Rulesets.Taiko.Audio -{ - public class DrumSampleMapping - { - private readonly ControlPointInfo controlPoints; - private readonly Dictionary mappings = new Dictionary(); - - public readonly List Sounds = new List(); - - public DrumSampleMapping(ControlPointInfo controlPoints) - { - this.controlPoints = controlPoints; - - IEnumerable samplePoints = controlPoints.SamplePoints.Count == 0 ? new[] { controlPoints.SamplePointAt(double.MinValue) } : controlPoints.SamplePoints; - - foreach (var s in samplePoints) - { - var centre = s.GetSampleInfo(); - var rim = s.GetSampleInfo(HitSampleInfo.HIT_CLAP); - - mappings[s.Time] = new DrumSample - { - Centre = addSound(centre), - Rim = addSound(rim) - }; - } - } - - private SkinnableSound addSound(HitSampleInfo hitSampleInfo) - { - var drawable = new SkinnableSound(hitSampleInfo); - Sounds.Add(drawable); - return drawable; - } - - public DrumSample SampleAt(double time) => mappings[controlPoints.SamplePointAt(time).Time]; - - public class DrumSample - { - public SkinnableSound Centre; - public SkinnableSound Rim; - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs index 81d645e294..b7b55b9ae0 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs @@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning public readonly Sprite Centre; [Resolved] - private DrumSampleMapping sampleMappings { get; set; } + private DrumSampleContainer sampleContainer { get; set; } public LegacyHalfDrum(bool flipped) { @@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning public bool OnPressed(TaikoAction action) { Drawable target = null; - var drumSample = sampleMappings.SampleAt(Time.Current); + var drumSample = sampleContainer.SampleAt(Time.Current); if (action == CentreAction) { diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs index 06ccd45cb8..f76f3d851a 100644 --- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs +++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs @@ -25,11 +25,11 @@ namespace osu.Game.Rulesets.Taiko.UI private const float middle_split = 0.025f; [Cached] - private DrumSampleMapping sampleMapping; + private DrumSampleContainer sampleContainer; public InputDrum(ControlPointInfo controlPoints) { - sampleMapping = new DrumSampleMapping(controlPoints); + sampleContainer = new DrumSampleContainer(controlPoints); RelativeSizeAxes = Axes.Both; } @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Taiko.UI } }); - AddRangeInternal(sampleMapping.Sounds); + AddRangeInternal(sampleContainer.Sounds); } /// @@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Taiko.UI private readonly Sprite centreHit; [Resolved] - private DrumSampleMapping sampleMappings { get; set; } + private DrumSampleContainer sampleContainer { get; set; } public TaikoHalfDrum(bool flipped) { @@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Taiko.UI Drawable target = null; Drawable back = null; - var drumSample = sampleMappings.SampleAt(Time.Current); + var drumSample = sampleContainer.SampleAt(Time.Current); if (action == CentreAction) { diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 49f9f01cff..fb9cab74c8 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -22,6 +22,9 @@ namespace osu.Game.Skinning [Resolved] private ISampleStore samples { get; set; } + public override bool RemoveWhenNotAlive => false; + public override bool RemoveCompletedTransforms => false; + public SkinnableSound(ISampleInfo hitSamples) : this(new[] { hitSamples }) { From 8e6a0493b4f972f2d577c91b0f8cecfd6a74ba8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 26 Jul 2020 15:15:01 +0200 Subject: [PATCH 78/78] Adjust InputDrum usage --- osu.Game.Rulesets.Taiko/UI/InputDrum.cs | 60 +++++++++++++------------ 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs index f76f3d851a..5966b24b34 100644 --- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs +++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs @@ -37,39 +37,41 @@ namespace osu.Game.Rulesets.Taiko.UI [BackgroundDependencyLoader] private void load() { - Child = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.InputDrum), _ => new Container + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - FillMode = FillMode.Fit, - Scale = new Vector2(0.9f), - Children = new Drawable[] + new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.InputDrum), _ => new Container { - new TaikoHalfDrum(false) + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + Scale = new Vector2(0.9f), + Children = new Drawable[] { - Name = "Left Half", - Anchor = Anchor.Centre, - Origin = Anchor.CentreRight, - RelativeSizeAxes = Axes.Both, - RelativePositionAxes = Axes.X, - X = -middle_split / 2, - RimAction = TaikoAction.LeftRim, - CentreAction = TaikoAction.LeftCentre - }, - new TaikoHalfDrum(true) - { - Name = "Right Half", - Anchor = Anchor.Centre, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.Both, - RelativePositionAxes = Axes.X, - X = middle_split / 2, - RimAction = TaikoAction.RightRim, - CentreAction = TaikoAction.RightCentre + new TaikoHalfDrum(false) + { + Name = "Left Half", + Anchor = Anchor.Centre, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.X, + X = -middle_split / 2, + RimAction = TaikoAction.LeftRim, + CentreAction = TaikoAction.LeftCentre + }, + new TaikoHalfDrum(true) + { + Name = "Right Half", + Anchor = Anchor.Centre, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.X, + X = middle_split / 2, + RimAction = TaikoAction.RightRim, + CentreAction = TaikoAction.RightCentre + } } - } - }); - - AddRangeInternal(sampleContainer.Sounds); + }), + sampleContainer + }; } ///