diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaMod.cs b/osu.Game.Rulesets.Mania/Mods/ManiaMod.cs index dfc9993bde..61e11f7610 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaMod.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaMod.cs @@ -105,6 +105,8 @@ namespace osu.Game.Rulesets.Mania.Mods public abstract class ManiaKeyMod : Mod { + // TODO: implement using the IApplicable interface. Haven't done so yet because KeyCount isn't even hooked up at the moment. + public override string ShortenedName => Name; public abstract int KeyCount { get; } public override double ScoreMultiplier => 1; // TODO: Implement the mania key mod score multiplier diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 6220bbd120..72ca9b37a8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Origin = Anchor.Centre; Position = HitObject.StackedPosition; - Scale = new Vector2(HitObject.Scale); + Scale = new Vector2(h.Scale); Children = new Drawable[] { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 5351ad50c4..bbe6b3a0a0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSpinner : DrawableOsuHitObject { - private readonly Spinner spinner; + protected readonly Spinner Spinner; public readonly SpinnerDisc Disc; public readonly SpinnerTicks Ticks; @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // we are slightly bigger than our parent, to clip the top and bottom of the circle Height = 1.3f; - spinner = s; + Spinner = s; Children = new Drawable[] { @@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Anchor = Anchor.Centre, Origin = Anchor.Centre, }, - Disc = new SpinnerDisc(spinner) + Disc = new SpinnerDisc(Spinner) { Scale = Vector2.Zero, Anchor = Anchor.Centre, @@ -115,7 +115,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables }; } - public float Progress => MathHelper.Clamp(Disc.RotationAbsolute / 360 / spinner.SpinsRequired, 0, 1); + public float Progress => MathHelper.Clamp(Disc.RotationAbsolute / 360 / Spinner.SpinsRequired, 0, 1); protected override void CheckForJudgements(bool userTriggered, double timeOffset) { @@ -136,7 +136,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables glow.FadeColour(completeColour, duration); } - if (!userTriggered && Time.Current >= spinner.EndTime) + if (!userTriggered && Time.Current >= Spinner.EndTime) { if (Progress >= 1) AddJudgement(new OsuJudgement { Result = HitResult.Great }); @@ -144,7 +144,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables AddJudgement(new OsuJudgement { Result = HitResult.Good }); else if (Progress > .75) AddJudgement(new OsuJudgement { Result = HitResult.Meh }); - else if (Time.Current >= spinner.EndTime) + else if (Time.Current >= Spinner.EndTime) AddJudgement(new OsuJudgement { Result = HitResult.Miss }); } } @@ -180,7 +180,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Ticks.Rotation = Disc.Rotation; spmCounter.SetRotation(Disc.RotationAbsolute); - float relativeCircleScale = spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight; + float relativeCircleScale = Spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight; Disc.ScaleTo(relativeCircleScale + (1 - relativeCircleScale) * Progress, 200, Easing.OutQuint); symbol.RotateTo(Disc.Rotation / 2, 500, Easing.OutQuint); @@ -190,22 +190,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.UpdatePreemptState(); - circleContainer.ScaleTo(spinner.Scale * 0.3f); - circleContainer.ScaleTo(spinner.Scale, TIME_PREEMPT / 1.4f, Easing.OutQuint); + circleContainer.ScaleTo(Spinner.Scale * 0.3f); + circleContainer.ScaleTo(Spinner.Scale, TIME_PREEMPT / 1.4f, Easing.OutQuint); Disc.RotateTo(-720); symbol.RotateTo(-720); mainContainer .ScaleTo(0) - .ScaleTo(spinner.Scale * circle.DrawHeight / DrawHeight * 1.4f, TIME_PREEMPT - 150, Easing.OutQuint) + .ScaleTo(Spinner.Scale * circle.DrawHeight / DrawHeight * 1.4f, TIME_PREEMPT - 150, Easing.OutQuint) .Then() .ScaleTo(1, 500, Easing.OutQuint); } protected override void UpdateCurrentState(ArmedState state) { - var sequence = this.Delay(spinner.Duration).FadeOut(160); + var sequence = this.Delay(Spinner.Duration).FadeOut(160); switch (state) { diff --git a/osu.Game.Rulesets.Osu/Tests/TestCaseHitCircle.cs b/osu.Game.Rulesets.Osu/Tests/TestCaseHitCircle.cs index cdce19ad21..f307ff7c70 100644 --- a/osu.Game.Rulesets.Osu/Tests/TestCaseHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Tests/TestCaseHitCircle.cs @@ -11,11 +11,13 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Tests.Visual; using OpenTK; -using osu.Game.Rulesets.Osu.Mods; using OpenTK.Graphics; using osu.Game.Rulesets.Osu.Judgements; using System.Collections.Generic; using System; +using osu.Game.Rulesets.Mods; +using System.Linq; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Tests { @@ -24,33 +26,34 @@ namespace osu.Game.Rulesets.Osu.Tests { public override IReadOnlyList RequiredTypes => new[] { - typeof(HitCircle), - typeof(OsuModHidden), typeof(DrawableHitCircle) }; private readonly Container content; protected override Container Content => content; - private bool auto; - private bool hidden; private int depthIndex; - private int circleSize; - private float circleScale = 1; + protected readonly List Mods = new List(); public TestCaseHitCircle() { base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 })); - AddStep("Single", () => testSingle()); - AddStep("Stream", testStream); - AddToggleStep("Auto", v => auto = v); - AddToggleStep("Hidden", v => hidden = v); - AddSliderStep("CircleSize", 0, 10, 0, s => circleSize = s); - AddSliderStep("CircleScale", 0.5f, 2, 1, s => circleScale = s); + AddStep("Miss Big Single", () => testSingle(2)); + AddStep("Miss Medium Single", () => testSingle(5)); + AddStep("Miss Small Single", () => testSingle(7)); + AddStep("Hit Big Single", () => testSingle(2, true)); + AddStep("Hit Medium Single", () => testSingle(5, true)); + AddStep("Hit Small Single", () => testSingle(7, true)); + AddStep("Miss Big Stream", () => testStream(2)); + AddStep("Miss Medium Stream", () => testStream(5)); + AddStep("Miss Small Stream", () => testStream(7)); + AddStep("Hit Big Stream", () => testStream(2, true)); + AddStep("Hit Medium Stream", () => testStream(5, true)); + AddStep("Hit Small Stream", () => testStream(7, true)); } - private void testSingle(double timeOffset = 0, Vector2? positionOffset = null) + private void testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null) { positionOffset = positionOffset ?? Vector2.Zero; @@ -66,27 +69,23 @@ namespace osu.Game.Rulesets.Osu.Tests var drawable = new TestDrawableHitCircle(circle, auto) { Anchor = Anchor.Centre, - Scale = new Vector2(circleScale), Depth = depthIndex++ }; - if (auto) - drawable.State.Value = ArmedState.Hit; - - if (hidden) - new OsuModHidden().ApplyToDrawableHitObjects(new [] { drawable }); + foreach (var mod in Mods.OfType()) + mod.ApplyToDrawableHitObjects(new[] { drawable }); Add(drawable); } - private void testStream() + private void testStream(float circleSize, bool auto = false) { - Vector2 pos = Vector2.Zero; + Vector2 pos = new Vector2(-250, 0); for (int i = 0; i <= 1000; i += 100) { - testSingle(i, pos); - pos += new Vector2(10); + testSingle(circleSize, auto, i, pos); + pos.X += 50; } } @@ -103,13 +102,15 @@ namespace osu.Game.Rulesets.Osu.Tests { if (auto && !userTriggered && timeOffset > 0) { - // pretend we really hit it + // force success AddJudgement(new OsuJudgement { - Result = HitObject.ScoreResultForOffset(timeOffset) + Result = HitResult.Great }); + State.Value = ArmedState.Hit; } - base.CheckForJudgements(userTriggered, timeOffset); + else + base.CheckForJudgements(userTriggered, timeOffset); } } } diff --git a/osu.Game.Rulesets.Osu/Tests/TestCaseHitCircleHidden.cs b/osu.Game.Rulesets.Osu/Tests/TestCaseHitCircleHidden.cs new file mode 100644 index 0000000000..7cc0c343a2 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Tests/TestCaseHitCircleHidden.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Game.Rulesets.Osu.Mods; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [Ignore("getting CI working")] + public class TestCaseHitCircleHidden : TestCaseHitCircle + { + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(OsuModHidden) }).ToList(); + + public TestCaseHitCircleHidden() + { + Mods.Add(new OsuModHidden()); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Tests/TestCaseSlider.cs b/osu.Game.Rulesets.Osu/Tests/TestCaseSlider.cs index 5b6b357351..1238572484 100644 --- a/osu.Game.Rulesets.Osu/Tests/TestCaseSlider.cs +++ b/osu.Game.Rulesets.Osu/Tests/TestCaseSlider.cs @@ -13,8 +13,10 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Tests.Visual; using OpenTK; -using osu.Game.Rulesets.Osu.Mods; using OpenTK.Graphics; +using osu.Game.Rulesets.Mods; +using System.Linq; +using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Osu.Tests { @@ -23,70 +25,94 @@ namespace osu.Game.Rulesets.Osu.Tests { public override IReadOnlyList RequiredTypes => new[] { - typeof(Slider), - typeof(HitCircle), - typeof(OsuModHidden), + typeof(SliderBall), + typeof(SliderBody), typeof(DrawableSlider), - typeof(DrawableHitCircle), - typeof(DrawableSliderTick), - typeof(DrawableRepeatPoint) + typeof(DrawableRepeatPoint), + typeof(DrawableOsuHitObject) }; private readonly Container content; protected override Container Content => content; - private bool hidden; - private int repeats; private int depthIndex; - private int circleSize; - private float circleScale = 1; - private double speedMultiplier = 2; - private double sliderMultiplier = 2; + protected readonly List Mods = new List(); public TestCaseSlider() { base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 })); - AddStep("Single", () => testSingle()); - AddStep("Stream", testStream); - AddStep("Repeated", () => testRepeated(repeats)); - AddToggleStep("Hidden", v => hidden = v); - AddSliderStep("Repeats", 1, 10, 1, s => repeats = s); - AddSliderStep("CircleSize", 0, 10, 0, s => circleSize = s); - AddSliderStep("CircleScale", 0.5f, 2, 1, s => circleScale = s); - AddSliderStep("SpeedMultiplier", 0.1, 10, 2, s => speedMultiplier = s); - AddSliderStep("SliderMultiplier", 0.1, 10, 2, s => sliderMultiplier = s); + AddStep("Big Single", () => testSimpleBig()); + AddStep("Medium Single", () => testSimpleMedium()); + AddStep("Small Single", () => testSimpleSmall()); + AddStep("Big 1 Repeat", () => testSimpleBig(1)); + AddStep("Medium 1 Repeat", () => testSimpleMedium(1)); + AddStep("Small 1 Repeat", () => testSimpleSmall(1)); + AddStep("Big 2 Repeats", () => testSimpleBig(2)); + AddStep("Medium 2 Repeats", () => testSimpleMedium(2)); + AddStep("Small 2 Repeats", () => testSimpleSmall(2)); + + AddStep("Slow Slider", testSlowSpeed); // slow long sliders take ages already so no repeat steps + AddStep("Slow Short Slider", () => testShortSlowSpeed()); + AddStep("Slow Short Slider 1 Repeats", () => testShortSlowSpeed(1)); + AddStep("Slow Short Slider 2 Repeats", () => testShortSlowSpeed(2)); + + AddStep("Fast Slider", () => testHighSpeed()); + AddStep("Fast Slider 1 Repeat", () => testHighSpeed(1)); + AddStep("Fast Slider 2 Repeats", () => testHighSpeed(2)); + AddStep("Fast Short Slider", () => testShortHighSpeed()); + AddStep("Fast Short Slider 1 Repeat", () => testShortHighSpeed(1)); + AddStep("Fast Short Slider 2 Repeats", () => testShortHighSpeed(2)); + + AddStep("Perfect Curve", testCurve); + // TODO more curve types? } - private void testSingle(double timeOffset = 0, Vector2? positionOffset = null) + private void testSimpleBig(int repeats = 0) => createSlider(2, repeats: repeats); + + private void testSimpleMedium(int repeats = 0) => createSlider(5, repeats: repeats); + + private void testSimpleSmall(int repeats = 0) => createSlider(7, repeats: repeats); + + private void testSlowSpeed() => createSlider(speedMultiplier: 0.5); + + private void testShortSlowSpeed(int repeats = 0) => createSlider(distance: 100, repeats: repeats, speedMultiplier: 0.5); + + private void testHighSpeed(int repeats = 0) => createSlider(repeats: repeats, speedMultiplier: 15); + + private void testShortHighSpeed(int repeats = 0) => createSlider(distance: 100, repeats: repeats, speedMultiplier: 15); + + private void createSlider(float circleSize = 2, float distance = 400, int repeats = 0, double speedMultiplier = 2) { - positionOffset = positionOffset ?? Vector2.Zero; + repeats++; // The first run through the slider is considered a repeat + + var repeatSamples = new List>(); + if (repeats > 1) + { + for (int i = 0; i < repeats; i++) + repeatSamples.Add(new List()); + } var slider = new Slider { - StartTime = Time.Current + 1000 + timeOffset, - Position = new Vector2(-200, 0) + positionOffset.Value, + StartTime = Time.Current + 1000, + Position = new Vector2(-(distance / 2), 0), ComboColour = Color4.LightSeaGreen, ControlPoints = new List { - new Vector2(-200, 0) + positionOffset.Value, - new Vector2(400, 0) + positionOffset.Value, + new Vector2(-(distance / 2), 0), + new Vector2(distance / 2, 0), }, - Distance = 400 + Distance = distance, + RepeatCount = repeats, + RepeatSamples = repeatSamples }; - addSlider(slider); + addSlider(slider, circleSize, speedMultiplier); } - private void testRepeated(int repeats) + private void testCurve() { - // The first run through the slider is considered a repeat - repeats++; - - var repeatSamples = new List>(); - for (int i = 0; i < repeats; i++) - repeatSamples.Add(new List()); - var slider = new Slider { StartTime = Time.Current + 1000, @@ -95,52 +121,32 @@ namespace osu.Game.Rulesets.Osu.Tests ControlPoints = new List { new Vector2(-200, 0), - new Vector2(400, 0), + new Vector2(0, 200), + new Vector2(200, 0) }, - Distance = 400, - RepeatCount = repeats, - RepeatSamples = repeatSamples + Distance = 600 }; - addSlider(slider); + addSlider(slider, 2, 3); } - private void testStream() - { - Vector2 pos = Vector2.Zero; - - for (int i = 0; i <= 1000; i += 100) - { - testSingle(i, pos); - pos += new Vector2(10); - } - } - - private void addSlider(Slider slider) + private void addSlider(Slider slider, float circleSize, double speedMultiplier) { var cpi = new ControlPointInfo(); cpi.DifficultyPoints.Add(new DifficultyControlPoint { SpeedMultiplier = speedMultiplier }); - var difficulty = new BeatmapDifficulty - { - SliderMultiplier = (float)sliderMultiplier, - CircleSize = circleSize - }; - - slider.ApplyDefaults(cpi, difficulty); + slider.ApplyDefaults(cpi, new BeatmapDifficulty { CircleSize = circleSize }); var drawable = new DrawableSlider(slider) { Anchor = Anchor.Centre, - Scale = new Vector2(circleScale), Depth = depthIndex++ }; - if (hidden) - new OsuModHidden().ApplyToDrawableHitObjects(new [] { drawable }); + foreach (var mod in Mods.OfType()) + mod.ApplyToDrawableHitObjects(new[] { drawable }); Add(drawable); } } - } diff --git a/osu.Game.Rulesets.Osu/Tests/TestCaseSliderHidden.cs b/osu.Game.Rulesets.Osu/Tests/TestCaseSliderHidden.cs new file mode 100644 index 0000000000..016909ad73 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Tests/TestCaseSliderHidden.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Game.Rulesets.Osu.Mods; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [Ignore("getting CI working")] + public class TestCaseSliderHidden : TestCaseSlider + { + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(OsuModHidden) }).ToList(); + + public TestCaseSliderHidden() + { + Mods.Add(new OsuModHidden()); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Tests/TestCaseSpinner.cs b/osu.Game.Rulesets.Osu/Tests/TestCaseSpinner.cs index c4ee56455a..752574018c 100644 --- a/osu.Game.Rulesets.Osu/Tests/TestCaseSpinner.cs +++ b/osu.Game.Rulesets.Osu/Tests/TestCaseSpinner.cs @@ -3,15 +3,16 @@ using System; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; -using OpenTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests @@ -21,46 +22,67 @@ namespace osu.Game.Rulesets.Osu.Tests { public override IReadOnlyList RequiredTypes => new[] { - typeof(Spinner), - typeof(OsuModHidden), - typeof(DrawableSpinner) + typeof(SpinnerDisc), + typeof(DrawableSpinner), + typeof(DrawableOsuHitObject) }; private readonly Container content; protected override Container Content => content; - private bool hidden; private int depthIndex; - private int circleSize; - private float circleScale = 1; + protected readonly List Mods = new List(); public TestCaseSpinner() { base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 })); - AddStep("Single", testSingle); - AddToggleStep("Hidden", v => hidden = v); - AddSliderStep("CircleSize", 0, 10, 0, s => circleSize = s); - AddSliderStep("CircleScale", 0.5f, 2, 1, s => circleScale = s); + AddStep("Miss Big", () => testSingle(2)); + AddStep("Miss Medium", () => testSingle(5)); + AddStep("Miss Small", () => testSingle(7)); + AddStep("Hit Big", () => testSingle(2, true)); + AddStep("Hit Medium", () => testSingle(5, true)); + AddStep("Hit Small", () => testSingle(7, true)); } - private void testSingle() + private void testSingle(float circleSize, bool auto = false) { var spinner = new Spinner { StartTime = Time.Current + 1000, EndTime = Time.Current + 4000 }; spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize }); - var drawable = new DrawableSpinner(spinner) + var drawable = new TestDrawableSpinner(spinner, auto) { Anchor = Anchor.Centre, - Scale = new Vector2(circleScale), Depth = depthIndex++ }; - if (hidden) - new OsuModHidden().ApplyToDrawableHitObjects(new [] { drawable }); + foreach (var mod in Mods.OfType()) + mod.ApplyToDrawableHitObjects(new[] { drawable }); Add(drawable); } + + private class TestDrawableSpinner : DrawableSpinner + { + private bool auto; + + public TestDrawableSpinner(Spinner s, bool auto) : base(s) + { + this.auto = auto; + } + + protected override void CheckForJudgements(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; + } + + base.CheckForJudgements(userTriggered, timeOffset); + } + } } } diff --git a/osu.Game.Rulesets.Osu/Tests/TestCaseSpinnerHidden.cs b/osu.Game.Rulesets.Osu/Tests/TestCaseSpinnerHidden.cs new file mode 100644 index 0000000000..9ef94b308f --- /dev/null +++ b/osu.Game.Rulesets.Osu/Tests/TestCaseSpinnerHidden.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Game.Rulesets.Osu.Mods; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [Ignore("getting CI working")] + public class TestCaseSpinnerHidden : TestCaseSpinner + { + public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(OsuModHidden) }).ToList(); + + public TestCaseSpinnerHidden() + { + Mods.Add(new OsuModHidden()); + } + } +} diff --git a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj index 785c3e17fb..05dec5a20d 100644 --- a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj +++ b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj @@ -88,9 +88,12 @@ + + + diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index 839932c640..a0904ee446 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -70,7 +70,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(new Vector2(320, 240), sprite.InitialPosition); Assert.IsTrue(sprite.IsDrawable); Assert.AreEqual(Anchor.Centre, sprite.Origin); - Assert.AreEqual("SB/lyric/ja-21.png", sprite.Path); + Assert.AreEqual(Path.Combine("SB", "lyric", "ja-21.png"), sprite.Path); var animation = background.Elements.ElementAt(12) as StoryboardAnimation; Assert.NotNull(animation); @@ -82,7 +82,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.IsTrue(animation.IsDrawable); Assert.AreEqual(AnimationLoopType.LoopForever, animation.LoopType); Assert.AreEqual(Anchor.Centre, animation.Origin); - Assert.AreEqual("SB/red jitter/red_0000.jpg", animation.Path); + Assert.AreEqual(Path.Combine("SB", "red jitter", "red_0000.jpg"), animation.Path); Assert.AreEqual(78993, animation.StartTime); } } diff --git a/osu.Game.Tests/Visual/TestCaseMods.cs b/osu.Game.Tests/Visual/TestCaseMods.cs index e535da3fcc..c78d3b1f9f 100644 --- a/osu.Game.Tests/Visual/TestCaseMods.cs +++ b/osu.Game.Tests/Visual/TestCaseMods.cs @@ -15,6 +15,8 @@ using System.Collections.Generic; using osu.Game.Rulesets.Osu; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Mania.Mods; using OpenTK.Graphics; namespace osu.Game.Tests.Visual @@ -68,6 +70,9 @@ namespace osu.Game.Tests.Visual case OsuRuleset or: testOsuMods(or); break; + case ManiaRuleset mr: + testManiaMods(mr); + break; } } } @@ -80,16 +85,27 @@ namespace osu.Game.Tests.Visual var noFailMod = easierMods.FirstOrDefault(m => m is OsuModNoFail); var hiddenMod = harderMods.FirstOrDefault(m => m is OsuModHidden); + var doubleTimeMod = harderMods.OfType().FirstOrDefault(m => m.Mods.Any(a => a is OsuModDoubleTime)); + var autoPilotMod = assistMods.FirstOrDefault(m => m is OsuModAutopilot); + var easy = easierMods.FirstOrDefault(m => m is OsuModEasy); + var hardRock = harderMods.FirstOrDefault(m => m is OsuModHardRock); + testSingleMod(noFailMod); testMultiMod(doubleTimeMod); - testIncompatibleMods(noFailMod, autoPilotMod); + testIncompatibleMods(easy, hardRock); testDeselectAll(easierMods.Where(m => !(m is MultiMod))); testMultiplierTextColour(noFailMod, modSelect.LowMultiplierColour); testMultiplierTextColour(hiddenMod, modSelect.HighMultiplierColour); - testMultiplierTextUnranked(autoPilotMod); + + testUnimplmentedMod(autoPilotMod); + } + + private void testManiaMods(ManiaRuleset ruleset) + { + testMultiplierTextUnranked(ruleset.GetModsFor(ModType.Special).First(m => m is ManiaModRandom)); } private void testSingleMod(Mod mod) @@ -124,6 +140,12 @@ namespace osu.Game.Tests.Visual checkNotSelected(mod); } + private void testUnimplmentedMod(Mod mod) + { + selectNext(mod); + checkNotSelected(mod); + } + private void testIncompatibleMods(Mod modA, Mod modB) { selectNext(modA); @@ -169,9 +191,9 @@ namespace osu.Game.Tests.Visual AddAssert("check for ranked", () => !modSelect.MultiplierLabel.Text.EndsWith(unranked_suffix)); } - private void selectNext(Mod mod) => AddStep($"left click {mod.Name}", () => modSelect.GetModButton(mod)?.SelectNext()); + private void selectNext(Mod mod) => AddStep($"left click {mod.Name}", () => modSelect.GetModButton(mod)?.SelectNext(1)); - private void selectPrevious(Mod mod) => AddStep($"right click {mod.Name}", () => modSelect.GetModButton(mod)?.SelectPrevious()); + private void selectPrevious(Mod mod) => AddStep($"right click {mod.Name}", () => modSelect.GetModButton(mod)?.SelectNext(-1)); private void checkSelected(Mod mod) { diff --git a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs index 18e40db064..3be4a18ec5 100644 --- a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs @@ -51,11 +51,12 @@ namespace osu.Game.Tests.Visual private class TestSongSelect : PlaySongSelect { public WorkingBeatmap CurrentBeatmap => Beatmap.Value; + public WorkingBeatmap CurrentBeatmapDetailsBeatmap => BeatmapDetails.Beatmap; public new BeatmapCarousel Carousel => base.Carousel; } [BackgroundDependencyLoader] - private void load(BeatmapManager baseManager) + private void load(OsuGameBase game) { TestSongSelect songSelect = null; @@ -69,12 +70,16 @@ namespace osu.Game.Tests.Visual dependencies.Cache(rulesets = new RulesetStore(contextFactory)); dependencies.Cache(manager = new BeatmapManager(storage, contextFactory, rulesets, null) { - DefaultBeatmap = defaultBeatmap = baseManager.GetWorkingBeatmap(null) + DefaultBeatmap = defaultBeatmap = game.Beatmap.Default }); void loadNewSongSelect(bool deleteMaps = false) => AddStep("reload song select", () => { - if (deleteMaps) manager.DeleteAll(); + if (deleteMaps) + { + manager.DeleteAll(); + game.Beatmap.SetDefault(); + } if (songSelect != null) { @@ -91,6 +96,8 @@ namespace osu.Game.Tests.Visual AddAssert("dummy selected", () => songSelect.CurrentBeatmap == defaultBeatmap); + AddAssert("dummy shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap == defaultBeatmap); + AddStep("import test maps", () => { for (int i = 0; i < 100; i += 10) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index c86860f7b0..47202c4534 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -19,6 +19,7 @@ using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.IO; using osu.Game.Database; using osu.Game.Graphics; +using osu.Game.Graphics.Textures; using osu.Game.IO; using osu.Game.IPC; using osu.Game.Online.API; @@ -101,15 +102,26 @@ namespace osu.Game.Beatmaps /// public Func GetStableStorage { private get; set; } + private void refreshImportContext() + { + lock (importContextLock) + { + importContext?.Value?.Dispose(); + + importContext = new Lazy(() => + { + var c = createContext(); + c.Database.AutoTransactionsEnabled = false; + return c; + }); + } + } + public BeatmapManager(Storage storage, Func context, RulesetStore rulesets, APIAccess api, IIpcHost importHost = null) { createContext = context; - importContext = new Lazy(() => - { - var c = createContext(); - c.Database.AutoTransactionsEnabled = false; - return c; - }); + + refreshImportContext(); beatmaps = createBeatmapStore(context); files = new FileStore(context, storage); @@ -174,13 +186,16 @@ namespace osu.Game.Beatmaps { e = e.InnerException ?? e; Logger.Error(e, $@"Could not import beatmap set ({Path.GetFileName(path)})"); + refreshImportContext(); } } notification.State = ProgressNotificationState.Completed; } - private readonly Lazy importContext; + private readonly object importContextLock = new object(); + + private Lazy importContext; /// /// Import a beatmap from an . @@ -189,7 +204,7 @@ namespace osu.Game.Beatmaps public BeatmapSetInfo Import(ArchiveReader archiveReader) { // let's only allow one concurrent import at a time for now. - lock (importContext) + lock (importContextLock) { var context = importContext.Value; @@ -314,7 +329,7 @@ namespace osu.Game.Beatmaps /// The beatmap set to delete. public void Delete(BeatmapSetInfo beatmapSet) { - lock (importContext) + lock (importContextLock) { var context = importContext.Value; @@ -377,7 +392,7 @@ namespace osu.Game.Beatmaps if (beatmapSet.Protected) return; - lock (importContext) + lock (importContextLock) { var context = importContext.Value; @@ -651,7 +666,7 @@ namespace osu.Game.Beatmaps try { - return new TextureStore(new RawTextureLoaderStore(store), false).Get(getPathForFile(Metadata.BackgroundFile)); + return new LargeTextureStore(new RawTextureLoaderStore(store)).Get(getPathForFile(Metadata.BackgroundFile)); } catch { diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index 8da6a0cefb..168f37e44e 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -266,6 +266,6 @@ namespace osu.Game.Beatmaps.Formats throw new InvalidDataException($@"Unknown origin: {value}"); } - private string cleanFilename(string path) => FileSafety.PathStandardise(path.Trim('\"')); + private string cleanFilename(string path) => FileSafety.PathSanitise(path.Trim('\"')); } } diff --git a/osu.Game/Graphics/Backgrounds/Background.cs b/osu.Game/Graphics/Backgrounds/Background.cs index 8eb2ddc0ab..4fb08a41f3 100644 --- a/osu.Game/Graphics/Backgrounds/Background.cs +++ b/osu.Game/Graphics/Backgrounds/Background.cs @@ -5,8 +5,8 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; using OpenTK.Graphics; +using osu.Game.Graphics.Textures; namespace osu.Game.Graphics.Backgrounds { @@ -22,7 +22,6 @@ namespace osu.Game.Graphics.Backgrounds this.textureName = textureName; RelativeSizeAxes = Axes.Both; - Depth = float.MaxValue; Add(Sprite = new Sprite { @@ -35,7 +34,7 @@ namespace osu.Game.Graphics.Backgrounds } [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load(LargeTextureStore textures) { if (!string.IsNullOrEmpty(textureName)) Sprite.Texture = textures.Get(textureName); diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index c788df3066..f67da52fc0 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -5,6 +5,8 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Graphics.Containers; +using osu.Framework.Input; +using OpenTK; namespace osu.Game.Graphics.Containers { @@ -22,6 +24,48 @@ namespace osu.Game.Graphics.Containers StateChanged += onStateChanged; } + /// + /// Whether mouse input should be blocked screen-wide while this overlay is visible. + /// Performing mouse actions outside of the valid extents will hide the overlay but pass the events through. + /// + public virtual bool BlockScreenWideMouse => BlockPassThroughMouse; + + // receive input outside our bounds so we can trigger a close event on ourselves. + public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => BlockScreenWideMouse || base.ReceiveMouseInputAt(screenSpacePos); + + protected override bool OnWheel(InputState state) + { + // always allow wheel to pass through to stuff outside our DrawRectangle. + if (!base.ReceiveMouseInputAt(state.Mouse.NativeState.Position)) + return false; + + return BlockPassThroughMouse; + } + + protected override bool OnClick(InputState state) + { + if (!base.ReceiveMouseInputAt(state.Mouse.NativeState.Position)) + { + State = Visibility.Hidden; + return true; + } + + return base.OnClick(state); + } + + protected override bool OnDragStart(InputState state) + { + if (!base.ReceiveMouseInputAt(state.Mouse.NativeState.Position)) + { + State = Visibility.Hidden; + return true; + } + + return base.OnDragStart(state); + } + + protected override bool OnDrag(InputState state) => State == Visibility.Hidden; + private void onStateChanged(Visibility visibility) { switch (visibility) diff --git a/osu.Game/Graphics/Textures/LargeTextureStore.cs b/osu.Game/Graphics/Textures/LargeTextureStore.cs new file mode 100644 index 0000000000..166364c8dd --- /dev/null +++ b/osu.Game/Graphics/Textures/LargeTextureStore.cs @@ -0,0 +1,18 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; + +namespace osu.Game.Graphics.Textures +{ + /// + /// A texture store that bypasses atlasing. + /// + public class LargeTextureStore : TextureStore + { + public LargeTextureStore(IResourceStore store = null) : base(store, false) + { + } + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 2bc32794d7..257b78ea0a 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -27,6 +27,7 @@ using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Screens.Play; using osu.Game.Input.Bindings; +using OpenTK.Graphics; namespace osu.Game { @@ -284,10 +285,10 @@ namespace osu.Game notifications.Enabled.BindTo(ShowOverlays); - ShowOverlays.ValueChanged += visible => + ShowOverlays.ValueChanged += show => { //central game screen change logic. - if (!visible) + if (!show) { hideAllOverlays(); musicController.State = Visibility.Hidden; @@ -331,10 +332,21 @@ namespace osu.Game } private Task asyncLoadStream; + private int visibleOverlayCount; private void loadComponentSingleFile(T d, Action add) where T : Drawable { + var focused = d as FocusedOverlayContainer; + if (focused != null) + { + focused.StateChanged += s => + { + visibleOverlayCount += s == Visibility.Visible ? 1 : -1; + screenStack.FadeColour(visibleOverlayCount > 0 ? OsuColour.Gray(0.5f) : Color4.White, 500, Easing.OutQuint); + }; + } + // schedule is here to ensure that all component loads are done after LoadComplete is run (and thus all dependencies are cached). // with some better organisation of LoadComplete to do construction and dependency caching in one step, followed by calls to loadComponentSingleFile, // we could avoid the need for scheduling altogether. diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index ea0bf22112..e311aea8e4 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -18,8 +18,10 @@ using osu.Game.Graphics; using osu.Game.Graphics.Cursor; using osu.Game.Online.API; using osu.Framework.Graphics.Performance; +using osu.Framework.Graphics.Textures; using osu.Framework.Logging; using osu.Game.Database; +using osu.Game.Graphics.Textures; using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.IO; @@ -89,6 +91,8 @@ namespace osu.Game { dependencies.Cache(contextFactory = new DatabaseContextFactory(Host)); + dependencies.Cache(new LargeTextureStore(new RawTextureLoaderStore(new NamespacedResourceStore(Resources, @"Textures")))); + dependencies.Cache(this); dependencies.Cache(LocalConfig); diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 9f40a08ad2..32e253f621 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -240,8 +240,6 @@ namespace osu.Game.Overlays public override bool AcceptsFocus => true; - protected override bool OnClick(InputState state) => true; - protected override void OnFocus(InputState state) { //this is necessary as textbox is masked away and therefore can't get focus :( diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs index 35c2e9234d..21f07bb0a4 100644 --- a/osu.Game/Overlays/Mods/ModButton.cs +++ b/osu.Game/Overlays/Mods/ModButton.cs @@ -32,7 +32,10 @@ namespace osu.Game.Overlays.Mods private readonly Container iconsContainer; private SampleChannel sampleOn, sampleOff; - public Action Action; // Passed the selected mod or null if none + /// + /// Fired when the selection changes. + /// + public Action SelectionChanged; public string TooltipText => (SelectedMod?.Description ?? Mods.FirstOrDefault()?.Description) ?? string.Empty; @@ -42,71 +45,73 @@ namespace osu.Game.Overlays.Mods // A selected index of -1 means not selected. private int selectedIndex = -1; - protected int SelectedIndex + /// + /// Change the selected mod index of this button. + /// + /// The new index. + /// Whether the selection changed. + private bool changeSelectedIndex(int newIndex) { - get + if (newIndex == selectedIndex) return false; + + int direction = newIndex < selectedIndex ? -1 : 1; + bool beforeSelected = Selected; + + Mod modBefore = SelectedMod ?? Mods[0]; + + if (newIndex >= Mods.Length) + newIndex = -1; + else if (newIndex < -1) + newIndex = Mods.Length - 1; + + if (newIndex >= 0 && !Mods[newIndex].HasImplementation) + return false; + + selectedIndex = newIndex; + Mod modAfter = SelectedMod ?? Mods[0]; + + if (beforeSelected != Selected) { - return selectedIndex; + iconsContainer.RotateTo(Selected ? 5f : 0f, 300, Easing.OutElastic); + iconsContainer.ScaleTo(Selected ? 1.1f : 1f, 300, Easing.OutElastic); } - set + + if (modBefore != modAfter) { - if (value == selectedIndex) return; + const float rotate_angle = 16; - int direction = value < selectedIndex ? -1 : 1; - bool beforeSelected = Selected; + foregroundIcon.RotateTo(rotate_angle * direction, mod_switch_duration, mod_switch_easing); + backgroundIcon.RotateTo(-rotate_angle * direction, mod_switch_duration, mod_switch_easing); - Mod modBefore = SelectedMod ?? Mods[0]; - - if (value >= Mods.Length) - selectedIndex = -1; - else if (value < -1) - selectedIndex = Mods.Length - 1; - else - selectedIndex = value; - - Mod modAfter = SelectedMod ?? Mods[0]; - - if (beforeSelected != Selected) + backgroundIcon.Icon = modAfter.Icon; + using (BeginDelayedSequence(mod_switch_duration, true)) { - iconsContainer.RotateTo(Selected ? 5f : 0f, 300, Easing.OutElastic); - iconsContainer.ScaleTo(Selected ? 1.1f : 1f, 300, Easing.OutElastic); + foregroundIcon + .RotateTo(-rotate_angle * direction) + .RotateTo(0f, mod_switch_duration, mod_switch_easing); + + backgroundIcon + .RotateTo(rotate_angle * direction) + .RotateTo(0f, mod_switch_duration, mod_switch_easing); + + Schedule(() => displayMod(modAfter)); } - - if (modBefore != modAfter) - { - const float rotate_angle = 16; - - foregroundIcon.RotateTo(rotate_angle * direction, mod_switch_duration, mod_switch_easing); - backgroundIcon.RotateTo(-rotate_angle * direction, mod_switch_duration, mod_switch_easing); - - backgroundIcon.Icon = modAfter.Icon; - using (BeginDelayedSequence(mod_switch_duration, true)) - { - foregroundIcon - .RotateTo(-rotate_angle * direction) - .RotateTo(0f, mod_switch_duration, mod_switch_easing); - - backgroundIcon - .RotateTo(rotate_angle * direction) - .RotateTo(0f, mod_switch_duration, mod_switch_easing); - - Schedule(() => displayMod(modAfter)); - } - } - - foregroundIcon.Highlighted = Selected; } + + foregroundIcon.Highlighted = Selected; + + (selectedIndex == -1 ? sampleOff : sampleOn).Play(); + SelectionChanged?.Invoke(SelectedMod); + return true; } - public bool Selected => SelectedIndex != -1; + public bool Selected => selectedIndex != -1; private Color4 selectedColour; + public Color4 SelectedColour { - get - { - return selectedColour; - } + get { return selectedColour; } set { if (value == selectedColour) return; @@ -116,12 +121,10 @@ namespace osu.Game.Overlays.Mods } private Mod mod; + public Mod Mod { - get - { - return mod; - } + get { return mod; } set { mod = value; @@ -147,9 +150,7 @@ namespace osu.Game.Overlays.Mods public Mod[] Mods { get; private set; } - // the mods from Mod, only multiple if Mod is a MultiMod - - public virtual Mod SelectedMod => Mods.ElementAtOrDefault(SelectedIndex); + public virtual Mod SelectedMod => Mods.ElementAtOrDefault(selectedIndex); [BackgroundDependencyLoader] private void load(AudioManager audio) @@ -163,31 +164,42 @@ namespace osu.Game.Overlays.Mods switch (args.Button) { case MouseButton.Left: - SelectNext(); + SelectNext(1); break; case MouseButton.Right: - SelectPrevious(); + SelectNext(-1); break; } + return true; } - public void SelectNext() + /// + /// Select the next available mod in a specified direction. + /// + /// 1 for forwards, -1 for backwards. + public void SelectNext(int direction) { - (++SelectedIndex == Mods.Length ? sampleOff : sampleOn).Play(); - Action?.Invoke(SelectedMod); + int start = selectedIndex + direction; + // wrap around if we are at an extremity. + if (start >= Mods.Length) + start = -1; + else if (start < -1) + start = Mods.Length - 1; + + for (int i = start; i < Mods.Length && i >= 0; i += direction) + { + if (Mods[i].HasImplementation) + { + changeSelectedIndex(i); + return; + } + } + + Deselect(); } - public void SelectPrevious() - { - (--SelectedIndex == -1 ? sampleOff : sampleOn).Play(); - Action?.Invoke(SelectedMod); - } - - public void Deselect() - { - SelectedIndex = -1; - } + public void Deselect() => changeSelectedIndex(-1); private void displayMod(Mod mod) { @@ -195,6 +207,7 @@ namespace osu.Game.Overlays.Mods backgroundIcon.Icon = foregroundIcon.Icon; foregroundIcon.Icon = mod.Icon; text.Text = mod.Name; + Colour = mod.HasImplementation ? Color4.White : Color4.Gray; } private void createIcons() @@ -264,7 +277,8 @@ namespace osu.Game.Overlays.Mods { public override string TooltipText => null; - public PassThroughTooltipModIcon(Mod mod) : base(mod) + public PassThroughTooltipModIcon(Mod mod) + : base(mod) { } } diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index aac8a72dd7..50310d1b27 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -51,7 +51,7 @@ namespace osu.Game.Overlays.Mods return new ModButton(m) { SelectedColour = selectedColour, - Action = Action, + SelectionChanged = Action, }; }).ToArray(); @@ -83,26 +83,33 @@ namespace osu.Game.Overlays.Mods { var index = Array.IndexOf(ToggleKeys, args.Key); if (index > -1 && index < buttons.Length) - buttons[index].SelectNext(); + buttons[index].SelectNext(state.Keyboard.ShiftPressed ? -1 : 1); return base.OnKeyDown(state, args); } - public void DeselectAll() - { - foreach (ModButton button in buttons) - button.Deselect(); - } + public void DeselectAll() => DeselectTypes(buttons.Select(b => b.SelectedMod?.GetType()).Where(t => t != null)); - public void DeselectTypes(Type[] modTypes) + /// + /// Deselect one or more mods in this section. + /// + /// The types of s which should be deselected. + /// Set to true to bypass animations and update selections immediately. + public void DeselectTypes(IEnumerable modTypes, bool immediate = false) { + int delay = 0; foreach (var button in buttons) { Mod selected = button.SelectedMod; if (selected == null) continue; foreach (Type type in modTypes) if (type.IsInstanceOfType(selected)) - button.Deselect(); + { + if (immediate) + button.Deselect(); + else + Scheduler.AddDelayed(() => button.Deselect(), delay += 50); + } } } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 9639907914..cc5a17358d 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -100,17 +100,22 @@ namespace osu.Game.Overlays.Mods refreshSelectedMods(); } - public void DeselectTypes(Type[] modTypes) + /// + /// Deselect one or more mods. + /// + /// The types of s which should be deselected. + /// Set to true to bypass animations and update selections immediately. + public void DeselectTypes(Type[] modTypes, bool immediate = false) { if (modTypes.Length == 0) return; foreach (ModSection section in ModSectionsContainer.Children) - section.DeselectTypes(modTypes); + section.DeselectTypes(modTypes, immediate); } private void modButtonPressed(Mod selectedMod) { if (selectedMod != null) - DeselectTypes(selectedMod.IncompatibleMods); + DeselectTypes(selectedMod.IncompatibleMods, true); refreshSelectedMods(); } @@ -127,10 +132,6 @@ namespace osu.Game.Overlays.Mods ranked &= mod.Ranked; } - // 1.00x - // 1.05x - // 1.20x - MultiplierLabel.Text = $"{multiplier:N2}x"; if (!ranked) MultiplierLabel.Text += " (Unranked)"; diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index b8f33c9a60..93ce3329df 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -65,10 +65,10 @@ namespace osu.Game.Overlays AlwaysPresent = true; } - protected override bool OnDragStart(InputState state) => true; - protected override bool OnDrag(InputState state) { + if (base.OnDrag(state)) return true; + Trace.Assert(state.Mouse.PositionMouseDown != null, "state.Mouse.PositionMouseDown != null"); Vector2 change = state.Mouse.Position - state.Mouse.PositionMouseDown.Value; @@ -77,7 +77,7 @@ namespace osu.Game.Overlays change *= change.Length <= 0 ? 0 : (float)Math.Pow(change.Length, 0.7f) / change.Length; dragContainer.MoveTo(change); - return base.OnDrag(state); + return true; } protected override bool OnDragEnd(InputState state) diff --git a/osu.Game/Overlays/Settings/Sections/AudioSection.cs b/osu.Game/Overlays/Settings/Sections/AudioSection.cs index c994a6296c..738c9e94d7 100644 --- a/osu.Game/Overlays/Settings/Sections/AudioSection.cs +++ b/osu.Game/Overlays/Settings/Sections/AudioSection.cs @@ -10,7 +10,7 @@ namespace osu.Game.Overlays.Settings.Sections public class AudioSection : SettingsSection { public override string Header => "Audio"; - public override FontAwesome Icon => FontAwesome.fa_headphones; + public override FontAwesome Icon => FontAwesome.fa_volume_up; public AudioSection() { @@ -23,4 +23,4 @@ namespace osu.Game.Overlays.Settings.Sections }; } } -} \ No newline at end of file +} diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index a80f6d4da8..f798d63e5a 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -177,8 +177,6 @@ namespace osu.Game.Overlays public override bool AcceptsFocus => true; - protected override bool OnClick(InputState state) => true; - protected override void OnFocus(InputState state) { GetContainingInputManager().ChangeFocus(searchTextBox); diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index e36db1f9da..ef1f7c8c70 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -62,10 +62,10 @@ namespace osu.Game.Overlays.Toolbar new ToolbarChatButton(), new ToolbarSocialButton(), new ToolbarMusicButton(), - new ToolbarButton - { - Icon = FontAwesome.fa_search - }, + //new ToolbarButton + //{ + // Icon = FontAwesome.fa_search + //}, userArea = new ToolbarUserArea(), new ToolbarNotificationButton(), } diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index 9aa660147a..c2e7fc5b44 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -34,15 +33,6 @@ namespace osu.Game.Overlays public const float CONTENT_X_MARGIN = 50; - // receive input outside our bounds so we can trigger a close event on ourselves. - public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => true; - - protected override bool OnClick(InputState state) - { - State = Visibility.Hidden; - return true; - } - public UserProfileOverlay() { FirstWaveColour = OsuColour.Gray(0.4f); diff --git a/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs b/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs new file mode 100644 index 0000000000..2d7cda5f1f --- /dev/null +++ b/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Rulesets.Mods +{ + /// + /// Represents a mod which can override (and block) a fail. + /// + public interface IApplicableFailOverride : IApplicableMod + { + /// + /// Whether we should allow failing at the current point in time. + /// + bool AllowFail { get; } + } +} diff --git a/osu.Game/Rulesets/Mods/IApplicableMod.cs b/osu.Game/Rulesets/Mods/IApplicableMod.cs new file mode 100644 index 0000000000..ed2b652598 --- /dev/null +++ b/osu.Game/Rulesets/Mods/IApplicableMod.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Rulesets.Mods +{ + /// + /// The base interface for a mod which can be applied in some way. + /// If this is not implemented by a mod, it will not be available for use in-game. + /// + public interface IApplicableMod + { + } +} diff --git a/osu.Game/Rulesets/Mods/IApplicableToClock.cs b/osu.Game/Rulesets/Mods/IApplicableToClock.cs index f0502cf346..8bb4e2b97d 100644 --- a/osu.Game/Rulesets/Mods/IApplicableToClock.cs +++ b/osu.Game/Rulesets/Mods/IApplicableToClock.cs @@ -8,8 +8,8 @@ namespace osu.Game.Rulesets.Mods /// /// An interface for mods that make adjustments to the track. /// - public interface IApplicableToClock + public interface IApplicableToClock : IApplicableMod { void ApplyToClock(IAdjustableClock clock); } -} \ No newline at end of file +} diff --git a/osu.Game/Rulesets/Mods/IApplicableToDifficulty.cs b/osu.Game/Rulesets/Mods/IApplicableToDifficulty.cs index 58f5defb5e..a95aa4370c 100644 --- a/osu.Game/Rulesets/Mods/IApplicableToDifficulty.cs +++ b/osu.Game/Rulesets/Mods/IApplicableToDifficulty.cs @@ -8,8 +8,8 @@ namespace osu.Game.Rulesets.Mods /// /// An interface for mods that make general adjustments to difficulty. /// - public interface IApplicableToDifficulty + public interface IApplicableToDifficulty : IApplicableMod { void ApplyToDifficulty(BeatmapDifficulty difficulty); } -} \ No newline at end of file +} diff --git a/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObject.cs b/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObject.cs index 1024d5686d..66dbc85095 100644 --- a/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObject.cs +++ b/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObject.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Mods /// /// An interface for s that can be applied to s. /// - public interface IApplicableToDrawableHitObjects + public interface IApplicableToDrawableHitObjects : IApplicableMod { /// /// Applies this to a list of s. diff --git a/osu.Game/Rulesets/Mods/IApplicableToHitObject.cs b/osu.Game/Rulesets/Mods/IApplicableToHitObject.cs index 7f39def343..1964ad728f 100644 --- a/osu.Game/Rulesets/Mods/IApplicableToHitObject.cs +++ b/osu.Game/Rulesets/Mods/IApplicableToHitObject.cs @@ -8,7 +8,7 @@ namespace osu.Game.Rulesets.Mods /// /// An interface for s that can be applied to s. /// - public interface IApplicableToHitObject + public interface IApplicableToHitObject : IApplicableMod where TObject : HitObject { /// diff --git a/osu.Game/Rulesets/Mods/IApplicableToRulesetContainer.cs b/osu.Game/Rulesets/Mods/IApplicableToRulesetContainer.cs index 9b23dd58f9..eae8c9d15c 100644 --- a/osu.Game/Rulesets/Mods/IApplicableToRulesetContainer.cs +++ b/osu.Game/Rulesets/Mods/IApplicableToRulesetContainer.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Mods /// /// An interface for s that can be applied to s. /// - public interface IApplicableToRulesetContainer + public interface IApplicableToRulesetContainer : IApplicableMod where TObject : HitObject { /// diff --git a/osu.Game/Rulesets/Mods/IApplicableToScoreProcessor.cs b/osu.Game/Rulesets/Mods/IApplicableToScoreProcessor.cs index db9b713c59..2314999d4f 100644 --- a/osu.Game/Rulesets/Mods/IApplicableToScoreProcessor.cs +++ b/osu.Game/Rulesets/Mods/IApplicableToScoreProcessor.cs @@ -8,7 +8,7 @@ namespace osu.Game.Rulesets.Mods /// /// An interface for mods that make general adjustments to score processor. /// - public interface IApplicableToScoreProcessor + public interface IApplicableToScoreProcessor : IApplicableMod { void ApplyToScoreProcessor(ScoreProcessor scoreProcessor); } diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 7b0034863e..68ed545701 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -41,6 +41,11 @@ namespace osu.Game.Rulesets.Mods /// public abstract double ScoreMultiplier { get; } + /// + /// Returns true if this mod is implemented (and playable). + /// + public virtual bool HasImplementation => this is IApplicableMod; + /// /// Returns if this mod is ranked. /// @@ -50,10 +55,5 @@ namespace osu.Game.Rulesets.Mods /// The mods this mod cannot be enabled with. /// public virtual Type[] IncompatibleMods => new Type[] { }; - - /// - /// Whether we should allow failing at the current point in time. - /// - public virtual bool AllowFail => true; } } diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index d94d4ba0db..bddb97f374 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mods } } - public class ModAutoplay : Mod + public class ModAutoplay : Mod, IApplicableFailOverride { public override string Name => "Autoplay"; public override string ShortenedName => "AT"; @@ -29,5 +29,6 @@ namespace osu.Game.Rulesets.Mods public override string Description => "Watch a perfect automated play through the song"; public override double ScoreMultiplier => 0; public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail) }; + public bool AllowFail => false; } } diff --git a/osu.Game/Rulesets/Mods/ModNoFail.cs b/osu.Game/Rulesets/Mods/ModNoFail.cs index 3a3878d77e..8aefd1b88e 100644 --- a/osu.Game/Rulesets/Mods/ModNoFail.cs +++ b/osu.Game/Rulesets/Mods/ModNoFail.cs @@ -6,7 +6,7 @@ using osu.Game.Graphics; namespace osu.Game.Rulesets.Mods { - public abstract class ModNoFail : Mod + public abstract class ModNoFail : Mod, IApplicableFailOverride { public override string Name => "NoFail"; public override string ShortenedName => "NF"; @@ -20,6 +20,6 @@ namespace osu.Game.Rulesets.Mods /// /// We never fail, 'yo. /// - public override bool AllowFail => false; + public bool AllowFail => false; } -} \ No newline at end of file +} diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index de84e90baf..36867a84d5 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Threading; using osu.Game.Graphics.Backgrounds; namespace osu.Game.Screens.Backgrounds @@ -24,16 +25,22 @@ namespace osu.Game.Screens.Backgrounds private void display(Background newBackground) { - current?.FadeOut(800, Easing.OutQuint); + current?.FadeOut(800, Easing.InOutSine); current?.Expire(); Add(current = newBackground); + currentDisplay++; } + private ScheduledDelegate nextTask; + public void Next() { - currentDisplay++; - LoadComponentAsync(new Background(backgroundName) { Depth = currentDisplay }, display); + nextTask?.Cancel(); + nextTask = Scheduler.AddDelayed(() => + { + LoadComponentAsync(new Background(backgroundName) { Depth = currentDisplay }, display); + }, 100); } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 35f39e940f..d4cfa0b3d4 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -298,7 +298,7 @@ namespace osu.Game.Screens.Play private bool onFail() { - if (Beatmap.Value.Mods.Value.Any(m => !m.AllowFail)) + if (Beatmap.Value.Mods.Value.OfType().Any(m => !m.AllowFail)) return false; decoupledClock.Stop(); diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index b343998e11..98acd0815d 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -229,11 +229,15 @@ namespace osu.Game.Screens.Select } } - public void SelectNextRandom() + /// + /// Select the next beatmap in the random sequence. + /// + /// True if a selection could be made, else False. + public bool SelectNextRandom() { var visible = beatmapSets.Where(s => !s.Filtered).ToList(); if (!visible.Any()) - return; + return false; if (selectedBeatmap != null) { @@ -263,6 +267,7 @@ namespace osu.Game.Screens.Select set = visible.ElementAt(RNG.Next(visible.Count)); select(set.Beatmaps.Skip(RNG.Next(set.Beatmaps.Count())).FirstOrDefault()); + return true; } public void SelectPreviousRandom() diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 729cb458c2..c25ba66475 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -20,6 +20,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; namespace osu.Game.Screens.Select { @@ -86,6 +87,8 @@ namespace osu.Game.Screens.Select public OsuSpriteText ArtistLabel { get; private set; } public FillFlowContainer MapperContainer { get; private set; } public FillFlowContainer InfoLabelContainer { get; private set; } + private UnicodeBindableString titleBinding; + private UnicodeBindableString artistBinding; public BufferedWedgeInfo(WorkingBeatmap working) { @@ -93,7 +96,7 @@ namespace osu.Game.Screens.Select } [BackgroundDependencyLoader] - private void load() + private void load(LocalisationEngine localisation) { var beatmapInfo = working.BeatmapInfo; var metadata = beatmapInfo.Metadata ?? working.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); @@ -102,6 +105,9 @@ namespace osu.Game.Screens.Select CacheDrawnFrameBuffer = true; RelativeSizeAxes = Axes.Both; + titleBinding = localisation.GetUnicodePreference(metadata.TitleUnicode, metadata.Title); + artistBinding = localisation.GetUnicodePreference(metadata.ArtistUnicode, metadata.Artist); + Children = new Drawable[] { // We will create the white-to-black gradient by modulating transparency and having @@ -167,13 +173,11 @@ namespace osu.Game.Screens.Select TitleLabel = new OsuSpriteText { Font = @"Exo2.0-MediumItalic", - Text = string.IsNullOrEmpty(metadata.Source) ? metadata.Title : metadata.Source + " — " + metadata.Title, TextSize = 28, }, ArtistLabel = new OsuSpriteText { Font = @"Exo2.0-MediumItalic", - Text = metadata.Artist, TextSize = 17, }, MapperContainer = new FillFlowContainer @@ -193,6 +197,15 @@ namespace osu.Game.Screens.Select } } }; + artistBinding.ValueChanged += value => setMetadata(metadata.Source); + artistBinding.TriggerChange(); + } + + private void setMetadata(string source) + { + ArtistLabel.Text = artistBinding.Value; + TitleLabel.Text = string.IsNullOrEmpty(source) ? titleBinding.Value : source + " — " + titleBinding.Value; + ForceRedraw(); } private InfoLabel[] getInfoLabels() diff --git a/osu.Game/Screens/Select/Leaderboards/Leaderboard.cs b/osu.Game/Screens/Select/Leaderboards/Leaderboard.cs index c15a179e8c..ac47fade48 100644 --- a/osu.Game/Screens/Select/Leaderboards/Leaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/Leaderboard.cs @@ -109,6 +109,13 @@ namespace osu.Game.Screens.Select.Leaderboards { if (value == placeholderState) return; + if (value != PlaceholderState.Successful) + { + getScoresRequest?.Cancel(); + getScoresRequest = null; + Scores = null; + } + switch (placeholderState = value) { case PlaceholderState.NetworkFailure: @@ -211,10 +218,6 @@ namespace osu.Game.Screens.Select.Leaderboards private void updateScores() { - getScoresRequest?.Cancel(); - getScoresRequest = null; - Scores = null; - if (Scope == LeaderboardScope.Local) { // TODO: get local scores from wherever here. @@ -234,16 +237,15 @@ namespace osu.Game.Screens.Select.Leaderboards return; } - PlaceholderState = PlaceholderState.Retrieving; - loading.Show(); - if (Scope != LeaderboardScope.Global && !api.LocalUser.Value.IsSupporter) { - loading.Hide(); PlaceholderState = PlaceholderState.NotSupporter; return; } + PlaceholderState = PlaceholderState.Retrieving; + loading.Show(); + getScoresRequest = new GetScoresRequest(Beatmap, osuGame?.Ruleset.Value ?? Beatmap.Ruleset, Scope); getScoresRequest.Success += r => { diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs index c66cc7beff..789064a5f1 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs @@ -25,6 +25,8 @@ namespace osu.Game.Screens.Select.Options private readonly Box holder; private readonly FillFlowContainer buttonsContainer; + public override bool BlockScreenWideMouse => false; + protected override void PopIn() { base.PopIn(); diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 87b3485dc1..a3997640ba 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Select { private OsuScreen player; private readonly ModSelectOverlay modSelect; - private readonly BeatmapDetailArea beatmapDetails; + protected readonly BeatmapDetailArea BeatmapDetails; private bool removeAutoModOnResume; public PlaySongSelect() @@ -35,13 +35,13 @@ namespace osu.Game.Screens.Select Anchor = Anchor.BottomCentre, }); - LeftContent.Add(beatmapDetails = new BeatmapDetailArea + LeftContent.Add(BeatmapDetails = new BeatmapDetailArea { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Top = 10, Right = 5 }, }); - beatmapDetails.Leaderboard.ScoreSelected += s => Push(new Results(s)); + BeatmapDetails.Leaderboard.ScoreSelected += s => Push(new Results(s)); } private SampleChannel sampleConfirm; @@ -78,7 +78,7 @@ namespace osu.Game.Screens.Select beatmap.Mods.BindTo(modSelect.SelectedMods); - beatmapDetails.Beatmap = beatmap; + BeatmapDetails.Beatmap = beatmap; if (beatmap.Track != null) beatmap.Track.Looping = true; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index b89a8a4e73..919cfcfbe4 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -449,9 +449,16 @@ namespace osu.Game.Screens.Select private void carouselBeatmapsLoaded() { if (!Beatmap.IsDefault && Beatmap.Value.BeatmapSetInfo?.DeletePending == false) + { Carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo); + } else if (Carousel.SelectedBeatmapSet == null) - Carousel.SelectNextRandom(); + { + if (!Carousel.SelectNextRandom()) + // in the case random selection failed, we want to trigger selectionChanged + // to show the dummy beatmap (we have nothing else to display). + carouselSelectionChanged(null); + } } private void delete(BeatmapSetInfo beatmap) diff --git a/osu.Game/Tests/Visual/TestCasePerformancePoints.cs b/osu.Game/Tests/Visual/TestCasePerformancePoints.cs index 8984fc843f..8b93be9bb5 100644 --- a/osu.Game/Tests/Visual/TestCasePerformancePoints.cs +++ b/osu.Game/Tests/Visual/TestCasePerformancePoints.cs @@ -7,6 +7,7 @@ using OpenTK; using OpenTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Caching; +using osu.Framework.Configuration; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -203,6 +204,8 @@ namespace osu.Game.Tests.Visual private readonly FillFlowContainer scores; private APIAccess api; + private readonly Bindable currentBeatmap = new Bindable(); + public PerformanceList() { RelativeSizeAxes = Axes.X; @@ -231,12 +234,15 @@ namespace osu.Game.Tests.Visual }; } - osuGame.Beatmap.ValueChanged += beatmapChanged; + currentBeatmap.ValueChanged += beatmapChanged; + currentBeatmap.BindTo(osuGame.Beatmap); } private GetScoresRequest lastRequest; private void beatmapChanged(WorkingBeatmap newBeatmap) { + if (!IsAlive) return; + lastRequest?.Cancel(); scores.Clear(); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index dfc2120b89..b244850946 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -267,6 +267,7 @@ + @@ -311,6 +312,8 @@ + +