diff --git a/osu.Android.props b/osu.Android.props
index 723844155f..d2bdbc8b61 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
-
+
diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
new file mode 100644
index 0000000000..a48ecb9b79
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs
@@ -0,0 +1,132 @@
+// 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 NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Catch.Skinning;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Skinning;
+using osu.Game.Tests.Visual;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Tests
+{
+ public class TestSceneHyperDashColouring : OsuTestScene
+ {
+ [Resolved]
+ private SkinManager skins { get; set; }
+
+ [Test]
+ public void TestDefaultFruitColour()
+ {
+ var skin = new TestSkin();
+
+ checkHyperDashFruitColour(skin, Catcher.DEFAULT_HYPER_DASH_COLOUR);
+ }
+
+ [Test]
+ public void TestCustomFruitColour()
+ {
+ var skin = new TestSkin
+ {
+ HyperDashFruitColour = Color4.Cyan
+ };
+
+ checkHyperDashFruitColour(skin, skin.HyperDashFruitColour);
+ }
+
+ [Test]
+ public void TestCustomFruitColourPriority()
+ {
+ var skin = new TestSkin
+ {
+ HyperDashColour = Color4.Goldenrod,
+ HyperDashFruitColour = Color4.Cyan
+ };
+
+ checkHyperDashFruitColour(skin, skin.HyperDashFruitColour);
+ }
+
+ [Test]
+ public void TestFruitColourFallback()
+ {
+ var skin = new TestSkin
+ {
+ HyperDashColour = Color4.Goldenrod
+ };
+
+ checkHyperDashFruitColour(skin, skin.HyperDashColour);
+ }
+
+ private void checkHyperDashFruitColour(ISkin skin, Color4 expectedColour)
+ {
+ DrawableFruit drawableFruit = null;
+
+ AddStep("create hyper-dash fruit", () =>
+ {
+ var fruit = new Fruit { HyperDashTarget = new Banana() };
+ fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
+
+ Child = setupSkinHierarchy(drawableFruit = new DrawableFruit(fruit)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Scale = new Vector2(4f),
+ }, skin);
+ });
+
+ AddAssert("hyper-dash colour is correct", () => checkLegacyFruitHyperDashColour(drawableFruit, expectedColour));
+ }
+
+ private Drawable setupSkinHierarchy(Drawable child, ISkin skin)
+ {
+ var legacySkinProvider = new SkinProvidingContainer(skins.GetSkin(DefaultLegacySkin.Info));
+ var testSkinProvider = new SkinProvidingContainer(skin);
+ var legacySkinTransformer = new SkinProvidingContainer(new CatchLegacySkinTransformer(testSkinProvider));
+
+ return legacySkinProvider
+ .WithChild(testSkinProvider
+ .WithChild(legacySkinTransformer
+ .WithChild(child)));
+ }
+
+ private bool checkLegacyFruitHyperDashColour(DrawableFruit fruit, Color4 expectedColour) =>
+ fruit.ChildrenOfType().First().Drawable.ChildrenOfType().Any(c => c.Colour == expectedColour);
+
+ private class TestSkin : LegacySkin
+ {
+ public Color4 HyperDashColour
+ {
+ get => Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()];
+ set => Configuration.CustomColours[CatchSkinColour.HyperDash.ToString()] = value;
+ }
+
+ public Color4 HyperDashAfterImageColour
+ {
+ get => Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()];
+ set => Configuration.CustomColours[CatchSkinColour.HyperDashAfterImage.ToString()] = value;
+ }
+
+ public Color4 HyperDashFruitColour
+ {
+ get => Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()];
+ set => Configuration.CustomColours[CatchSkinColour.HyperDashFruit.ToString()] = value;
+ }
+
+ public TestSkin()
+ : base(new SkinInfo(), null, null, string.Empty)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs
index 5797588ded..7ac9f11ad6 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs
@@ -7,6 +7,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK.Graphics;
@@ -67,7 +68,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- BorderColour = Color4.Red,
+ BorderColour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
BorderThickness = 12f * RADIUS_ADJUST,
Children = new Drawable[]
{
@@ -77,7 +78,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
Alpha = 0.3f,
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
- Colour = Color4.Red,
+ Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
}
}
});
diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
index 65e6e6f209..4a87eb95e7 100644
--- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs
@@ -65,6 +65,15 @@ namespace osu.Game.Rulesets.Catch.Skinning
public SampleChannel GetSample(ISampleInfo sample) => source.GetSample(sample);
- public IBindable GetConfig(TLookup lookup) => source.GetConfig(lookup);
+ public IBindable GetConfig(TLookup lookup)
+ {
+ switch (lookup)
+ {
+ case CatchSkinColour colour:
+ return source.GetConfig(new SkinCustomColourLookup(colour));
+ }
+
+ return source.GetConfig(lookup);
+ }
}
}
diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchSkinColour.cs b/osu.Game.Rulesets.Catch/Skinning/CatchSkinColour.cs
new file mode 100644
index 0000000000..4506111498
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/CatchSkinColour.cs
@@ -0,0 +1,23 @@
+// 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.Catch.Skinning
+{
+ public enum CatchSkinColour
+ {
+ ///
+ /// The colour to be used for the catcher while in hyper-dashing state.
+ ///
+ HyperDash,
+
+ ///
+ /// The colour to be used for fruits that grant the catcher the ability to hyper-dash.
+ ///
+ HyperDashFruit,
+
+ ///
+ /// The colour to be used for the "exploding" catcher sprite on beginning of hyper-dashing.
+ ///
+ HyperDashAfterImage,
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs
index 25ee0811d0..5be54d3882 100644
--- a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs
@@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Catch.Objects.Drawables;
+using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Skinning;
using osuTK;
@@ -55,14 +56,16 @@ namespace osu.Game.Rulesets.Catch.Skinning
{
var hyperDash = new Sprite
{
- Texture = skin.GetTexture(lookupName),
- Colour = Color4.Red,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Blending = BlendingParameters.Additive,
Depth = 1,
Alpha = 0.7f,
- Scale = new Vector2(1.2f)
+ Scale = new Vector2(1.2f),
+ Texture = skin.GetTexture(lookupName),
+ Colour = skin.GetConfig(CatchSkinColour.HyperDashFruit)?.Value ??
+ skin.GetConfig(CatchSkinColour.HyperDash)?.Value ??
+ Catcher.DEFAULT_HYPER_DASH_COLOUR,
};
AddInternal(hyperDash);
diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs
index 7c815370c8..920d804e72 100644
--- a/osu.Game.Rulesets.Catch/UI/Catcher.cs
+++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs
@@ -21,6 +21,8 @@ namespace osu.Game.Rulesets.Catch.UI
{
public class Catcher : Container, IKeyBindingHandler
{
+ public static readonly Color4 DEFAULT_HYPER_DASH_COLOUR = Color4.Red;
+
///
/// Whether we are hyper-dashing or not.
///
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs
new file mode 100644
index 0000000000..8bd3d3c7cc
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs
@@ -0,0 +1,106 @@
+// 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 NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Osu.Mods;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Osu.Tests.Mods
+{
+ public class TestSceneOsuModHidden : ModTestScene
+ {
+ public TestSceneOsuModHidden()
+ : base(new OsuRuleset())
+ {
+ }
+
+ [Test]
+ public void TestDefaultBeatmapTest() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModHidden(),
+ Autoplay = true,
+ PassCondition = checkSomeHit
+ });
+
+ [Test]
+ public void FirstCircleAfterTwoSpinners() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModHidden(),
+ Autoplay = true,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Spinner
+ {
+ Position = new Vector2(256, 192),
+ EndTime = 1000,
+ },
+ new Spinner
+ {
+ Position = new Vector2(256, 192),
+ StartTime = 1200,
+ EndTime = 2200,
+ },
+ new HitCircle
+ {
+ Position = new Vector2(300, 192),
+ StartTime = 3200,
+ },
+ new HitCircle
+ {
+ Position = new Vector2(384, 192),
+ StartTime = 4200,
+ }
+ }
+ },
+ PassCondition = checkSomeHit
+ });
+
+ [Test]
+ public void FirstSliderAfterTwoSpinners() => CreateModTest(new ModTestData
+ {
+ Mod = new OsuModHidden(),
+ Autoplay = true,
+ Beatmap = new Beatmap
+ {
+ HitObjects = new List
+ {
+ new Spinner
+ {
+ Position = new Vector2(256, 192),
+ EndTime = 1000,
+ },
+ new Spinner
+ {
+ Position = new Vector2(256, 192),
+ StartTime = 1200,
+ EndTime = 2200,
+ },
+ new Slider
+ {
+ StartTime = 3200,
+ Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), })
+ },
+ new Slider
+ {
+ StartTime = 5200,
+ Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), })
+ }
+ }
+ },
+ PassCondition = checkSomeHit
+ });
+
+ private bool checkSomeHit()
+ {
+ return Player.ScoreProcessor.JudgedHits >= 4;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
index d6858f831e..40ee53e8f2 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs
@@ -296,6 +296,44 @@ namespace osu.Game.Rulesets.Osu.Tests
addJudgementAssert(hitObjects[1], HitResult.Great);
}
+ [Test]
+ public void TestHitSliderHeadBeforeHitCircle()
+ {
+ const double time_circle = 1000;
+ const double time_slider = 1200;
+ Vector2 positionCircle = Vector2.Zero;
+ Vector2 positionSlider = new Vector2(80);
+
+ var hitObjects = new List
+ {
+ new TestHitCircle
+ {
+ StartTime = time_circle,
+ Position = positionCircle
+ },
+ new TestSlider
+ {
+ StartTime = time_slider,
+ Position = positionSlider,
+ Path = new SliderPath(PathType.Linear, new[]
+ {
+ Vector2.Zero,
+ new Vector2(25, 0),
+ })
+ }
+ };
+
+ performTest(hitObjects, new List
+ {
+ new OsuReplayFrame { Time = time_circle - 100, Position = positionSlider, Actions = { OsuAction.LeftButton } },
+ new OsuReplayFrame { Time = time_circle, Position = positionCircle, Actions = { OsuAction.RightButton } },
+ new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } },
+ });
+
+ addJudgementAssert(hitObjects[0], HitResult.Great);
+ addJudgementAssert(hitObjects[1], HitResult.Great);
+ }
+
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
{
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
@@ -371,6 +409,9 @@ namespace osu.Game.Rulesets.Osu.Tests
{
HeadCircle.HitWindows = new TestHitWindows();
TailCircle.HitWindows = new TestHitWindows();
+
+ HeadCircle.HitWindows.SetDifficulty(0);
+ TailCircle.HitWindows.SetDifficulty(0);
};
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
index 91a4e049e3..fdba03f260 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs
@@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Mods
private const double fade_in_duration_multiplier = 0.4;
private const double fade_out_duration_multiplier = 0.3;
+ protected override bool IsFirstHideableObject(DrawableHitObject hitObject) => !(hitObject is DrawableSpinner);
+
public override void ApplyToDrawableHitObjects(IEnumerable drawables)
{
static void adjustFadeIn(OsuHitObject h) => h.TimeFadeIn = h.TimePreempt * fade_in_duration_multiplier;
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index 522217a916..72502c02cd 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -125,7 +125,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return new DrawableSliderTail(slider, tail);
case SliderHeadCircle head:
- return new DrawableSliderHead(slider, head) { OnShake = Shake };
+ return new DrawableSliderHead(slider, head)
+ {
+ OnShake = Shake,
+ CheckHittable = (d, t) => CheckHittable?.Invoke(d, t) ?? true
+ };
case SliderTick tick:
return new DrawableSliderTick(tick) { Position = tick.Position - slider.Position };
diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs
index dfca2aff7b..8e4f81347d 100644
--- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs
+++ b/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs
@@ -1,16 +1,17 @@
// 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 osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.UI
{
///
- /// Ensures that s are hit in-order.
+ /// Ensures that s are hit in-order. Affectionately known as "note lock".
/// If a is hit out of order:
///
/// - The hit is blocked if it occurred earlier than the previous 's start time.
@@ -36,13 +37,9 @@ namespace osu.Game.Rulesets.Osu.UI
{
DrawableHitObject blockingObject = null;
- // Find the last hitobject which blocks future hits.
- foreach (var obj in hitObjectContainer.AliveObjects)
+ foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime))
{
- if (obj == hitObject)
- break;
-
- if (drawableCanBlockFutureHits(obj))
+ if (hitObjectCanBlockFutureHits(obj))
blockingObject = obj;
}
@@ -54,74 +51,56 @@ namespace osu.Game.Rulesets.Osu.UI
// 1. The last blocking hitobject has been judged.
// 2. The current time is after the last hitobject's start time.
// Hits at exactly the same time as the blocking hitobject are allowed for maps that contain simultaneous hitobjects (e.g. /b/372245).
- if (blockingObject.Judged || time >= blockingObject.HitObject.StartTime)
- return true;
-
- return false;
+ return blockingObject.Judged || time >= blockingObject.HitObject.StartTime;
}
///
/// Handles a being hit to potentially miss all earlier s.
///
/// The that was hit.
- public void HandleHit(HitObject hitObject)
+ public void HandleHit(DrawableHitObject hitObject)
{
// Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners).
if (!hitObjectCanBlockFutureHits(hitObject))
return;
- double maximumTime = hitObject.StartTime;
+ if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset))
+ throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!");
- // Iterate through and apply miss results to all top-level and nested hitobjects which block future hits.
- foreach (var obj in hitObjectContainer.AliveObjects)
+ foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime))
{
- if (obj.Judged || obj.HitObject.StartTime >= maximumTime)
+ if (obj.Judged)
continue;
- if (hitObjectCanBlockFutureHits(obj.HitObject))
- applyMiss(obj);
-
- foreach (var nested in obj.NestedHitObjects)
- {
- if (nested.Judged || nested.HitObject.StartTime >= maximumTime)
- continue;
-
- if (hitObjectCanBlockFutureHits(nested.HitObject))
- applyMiss(nested);
- }
+ if (hitObjectCanBlockFutureHits(obj))
+ ((DrawableOsuHitObject)obj).MissForcefully();
}
-
- static void applyMiss(DrawableHitObject obj) => ((DrawableOsuHitObject)obj).MissForcefully();
- }
-
- ///
- /// Whether a blocks hits on future s until its start time is reached.
- ///
- ///
- /// This will ONLY match on top-most s.
- ///
- /// The to test.
- private static bool drawableCanBlockFutureHits(DrawableHitObject hitObject)
- {
- // Special considerations for slider tails aren't required since only top-most drawable hitobjects are being iterated over.
- return hitObject is DrawableHitCircle || hitObject is DrawableSlider;
}
///
/// Whether a blocks hits on future s until its start time is reached.
///
- ///
- /// This is more rigorous and may not match on top-most s as does.
- ///
/// The to test.
- private static bool hitObjectCanBlockFutureHits(HitObject hitObject)
- {
- // Unlike the above we will receive slider tails, but they do not block future hits.
- if (hitObject is SliderTailCircle)
- return false;
+ private static bool hitObjectCanBlockFutureHits(DrawableHitObject hitObject)
+ => hitObject is DrawableHitCircle;
- // All other hitcircles continue to block future hits.
- return hitObject is HitCircle;
+ private IEnumerable enumerateHitObjectsUpTo(double targetTime)
+ {
+ foreach (var obj in hitObjectContainer.AliveObjects)
+ {
+ if (obj.HitObject.StartTime >= targetTime)
+ yield break;
+
+ yield return obj;
+
+ foreach (var nestedObj in obj.NestedHitObjects)
+ {
+ if (nestedObj.HitObject.StartTime >= targetTime)
+ break;
+
+ yield return nestedObj;
+ }
+ }
}
}
}
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index 2f222f59b4..4b1a2ce43c 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Osu.UI
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)
{
// Hitobjects that block future hits should miss previous hitobjects if they're hit out-of-order.
- hitPolicy.HandleHit(result.HitObject);
+ hitPolicy.HandleHit(judgedObject);
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
diff --git a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
index 2782e902fe..158954106d 100644
--- a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
+++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs
@@ -29,11 +29,17 @@ namespace osu.Game.Tests.NonVisual
var cpi = new ControlPointInfo();
cpi.Add(0, new TimingControlPoint()); // is *not* redundant, special exception for first timing point.
- cpi.Add(1000, new TimingControlPoint()); // is redundant
+ cpi.Add(1000, new TimingControlPoint()); // is also not redundant, due to change of offset
- Assert.That(cpi.Groups.Count, Is.EqualTo(1));
- Assert.That(cpi.TimingPoints.Count, Is.EqualTo(1));
- Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1));
+ Assert.That(cpi.Groups.Count, Is.EqualTo(2));
+ Assert.That(cpi.TimingPoints.Count, Is.EqualTo(2));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2));
+
+ cpi.Add(1000, new TimingControlPoint()); //is redundant
+
+ Assert.That(cpi.Groups.Count, Is.EqualTo(2));
+ Assert.That(cpi.TimingPoints.Count, Is.EqualTo(2));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2));
}
[Test]
@@ -86,11 +92,12 @@ namespace osu.Game.Tests.NonVisual
Assert.That(cpi.EffectPoints.Count, Is.EqualTo(0));
Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0));
- cpi.Add(1000, new EffectControlPoint { KiaiMode = true }); // is not redundant
+ cpi.Add(1000, new EffectControlPoint { KiaiMode = true, OmitFirstBarLine = true }); // is not redundant
+ cpi.Add(1400, new EffectControlPoint { KiaiMode = true, OmitFirstBarLine = true }); // same settings, but is not redundant
- Assert.That(cpi.Groups.Count, Is.EqualTo(1));
- Assert.That(cpi.EffectPoints.Count, Is.EqualTo(1));
- Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(1));
+ Assert.That(cpi.Groups.Count, Is.EqualTo(2));
+ Assert.That(cpi.EffectPoints.Count, Is.EqualTo(2));
+ Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(2));
}
[Test]
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs
new file mode 100644
index 0000000000..db65e91d17
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs
@@ -0,0 +1,98 @@
+// 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 NUnit.Framework;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Input.Bindings;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Difficulty;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.UI;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ [HeadlessTest]
+ public class TestSceneKeyBindings : OsuManualInputManagerTestScene
+ {
+ private readonly ActionReceiver receiver;
+
+ public TestSceneKeyBindings()
+ {
+ Add(new TestKeyBindingContainer
+ {
+ Child = receiver = new ActionReceiver()
+ });
+ }
+
+ [Test]
+ public void TestDefaultsWhenNotDatabased()
+ {
+ AddStep("fire key", () =>
+ {
+ InputManager.PressKey(Key.A);
+ InputManager.ReleaseKey(Key.A);
+ });
+
+ AddAssert("received key", () => receiver.ReceivedAction);
+ }
+
+ private class TestRuleset : Ruleset
+ {
+ public override IEnumerable GetModsFor(ModType type) =>
+ throw new System.NotImplementedException();
+
+ public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) =>
+ throw new System.NotImplementedException();
+
+ public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) =>
+ throw new System.NotImplementedException();
+
+ public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) =>
+ throw new System.NotImplementedException();
+
+ public override IEnumerable GetDefaultKeyBindings(int variant = 0)
+ {
+ return new[]
+ {
+ new KeyBinding(InputKey.A, TestAction.Down),
+ };
+ }
+
+ public override string Description => "test";
+ public override string ShortName => "test";
+ }
+
+ private enum TestAction
+ {
+ Down,
+ }
+
+ private class TestKeyBindingContainer : DatabasedKeyBindingContainer
+ {
+ public TestKeyBindingContainer()
+ : base(new TestRuleset().RulesetInfo, 0)
+ {
+ }
+ }
+
+ private class ActionReceiver : CompositeDrawable, IKeyBindingHandler
+ {
+ public bool ReceivedAction;
+
+ public bool OnPressed(TestAction action)
+ {
+ ReceivedAction = action == TestAction.Down;
+ return true;
+ }
+
+ public void OnReleased(TestAction action)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs
new file mode 100644
index 0000000000..df95f24686
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneDashboardOverlay.cs
@@ -0,0 +1,43 @@
+// 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 NUnit.Framework;
+using osu.Game.Overlays;
+using osu.Game.Overlays.Dashboard;
+using osu.Game.Overlays.Dashboard.Friends;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ public class TestSceneDashboardOverlay : OsuTestScene
+ {
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(DashboardOverlay),
+ typeof(DashboardOverlayHeader),
+ typeof(FriendDisplay)
+ };
+
+ protected override bool UseOnlineAPI => true;
+
+ private readonly DashboardOverlay overlay;
+
+ public TestSceneDashboardOverlay()
+ {
+ Add(overlay = new DashboardOverlay());
+ }
+
+ [Test]
+ public void TestShow()
+ {
+ AddStep("Show", overlay.Show);
+ }
+
+ [Test]
+ public void TestHide()
+ {
+ AddStep("Hide", overlay.Hide);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs
index cf365a7614..0b5ff1c960 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs
@@ -10,6 +10,7 @@ using osu.Game.Users;
using osu.Game.Overlays;
using osu.Framework.Allocation;
using NUnit.Framework;
+using osu.Game.Online.API;
namespace osu.Game.Tests.Visual.Online
{
@@ -27,7 +28,7 @@ namespace osu.Game.Tests.Visual.Online
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
- private FriendDisplay display;
+ private TestFriendDisplay display;
[SetUp]
public void Setup() => Schedule(() =>
@@ -35,7 +36,7 @@ namespace osu.Game.Tests.Visual.Online
Child = new BasicScrollContainer
{
RelativeSizeAxes = Axes.Both,
- Child = display = new FriendDisplay()
+ Child = display = new TestFriendDisplay()
};
});
@@ -83,5 +84,17 @@ namespace osu.Game.Tests.Visual.Online
LastVisit = DateTimeOffset.Now
}
};
+
+ private class TestFriendDisplay : FriendDisplay
+ {
+ public void Fetch()
+ {
+ base.APIStateChanged(API, APIState.Online);
+ }
+
+ public override void APIStateChanged(IAPIProvider api, APIState state)
+ {
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs
new file mode 100644
index 0000000000..103308d34d
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs
@@ -0,0 +1,85 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Online.Chat;
+using osu.Game.Rulesets;
+using osu.Game.Users;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ [HeadlessTest]
+ public class TestSceneNowPlayingCommand : OsuTestScene
+ {
+ [Cached(typeof(IChannelPostTarget))]
+ private PostTarget postTarget { get; set; }
+
+ public TestSceneNowPlayingCommand()
+ {
+ Add(postTarget = new PostTarget());
+ }
+
+ [Test]
+ public void TestGenericActivity()
+ {
+ AddStep("Set activity", () => API.Activity.Value = new UserActivity.InLobby());
+
+ AddStep("Run command", () => Add(new NowPlayingCommand()));
+
+ AddAssert("Check correct response", () => postTarget.LastMessage.Contains("is listening"));
+ }
+
+ [Test]
+ public void TestEditActivity()
+ {
+ AddStep("Set activity", () => API.Activity.Value = new UserActivity.Editing(new BeatmapInfo()));
+
+ AddStep("Run command", () => Add(new NowPlayingCommand()));
+
+ AddAssert("Check correct response", () => postTarget.LastMessage.Contains("is editing"));
+ }
+
+ [Test]
+ public void TestPlayActivity()
+ {
+ AddStep("Set activity", () => API.Activity.Value = new UserActivity.SoloGame(new BeatmapInfo(), new RulesetInfo()));
+
+ AddStep("Run command", () => Add(new NowPlayingCommand()));
+
+ AddAssert("Check correct response", () => postTarget.LastMessage.Contains("is playing"));
+ }
+
+ [TestCase(true)]
+ [TestCase(false)]
+ public void TestLinkPresence(bool hasOnlineId)
+ {
+ AddStep("Set activity", () => API.Activity.Value = new UserActivity.InLobby());
+
+ AddStep("Set beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(null, null)
+ {
+ BeatmapInfo = { OnlineBeatmapID = hasOnlineId ? 1234 : (int?)null }
+ });
+
+ AddStep("Run command", () => Add(new NowPlayingCommand()));
+
+ if (hasOnlineId)
+ AddAssert("Check link presence", () => postTarget.LastMessage.Contains("https://osu.ppy.sh/b/1234"));
+ else
+ AddAssert("Check link not present", () => !postTarget.LastMessage.Contains("https://"));
+ }
+
+ public class PostTarget : Component, IChannelPostTarget
+ {
+ public void PostMessage(string text, bool isAction = false, Channel target = null)
+ {
+ LastMessage = text;
+ }
+
+ public string LastMessage { get; private set; }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index 4405c75744..39e04ed39a 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -359,6 +359,68 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmap == null);
}
+ [Test]
+ public void TestPresentNewRulesetNewBeatmap()
+ {
+ createSongSelect();
+ changeRuleset(2);
+
+ addRulesetImportStep(2);
+ AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 2);
+
+ addRulesetImportStep(0);
+ addRulesetImportStep(0);
+ addRulesetImportStep(0);
+
+ BeatmapInfo target = null;
+
+ AddStep("select beatmap/ruleset externally", () =>
+ {
+ target = manager.GetAllUsableBeatmapSets()
+ .Last(b => b.Beatmaps.Any(bi => bi.RulesetID == 0)).Beatmaps.Last();
+
+ Ruleset.Value = rulesets.AvailableRulesets.First(r => r.ID == 0);
+ Beatmap.Value = manager.GetWorkingBeatmap(target);
+ });
+
+ AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.Equals(target));
+
+ // this is an important check, to make sure updateComponentFromBeatmap() was actually run
+ AddUntilStep("selection shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap.BeatmapInfo == target);
+ }
+
+ [Test]
+ public void TestPresentNewBeatmapNewRuleset()
+ {
+ createSongSelect();
+ changeRuleset(2);
+
+ addRulesetImportStep(2);
+ AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 2);
+
+ addRulesetImportStep(0);
+ addRulesetImportStep(0);
+ addRulesetImportStep(0);
+
+ BeatmapInfo target = null;
+
+ AddStep("select beatmap/ruleset externally", () =>
+ {
+ target = manager.GetAllUsableBeatmapSets()
+ .Last(b => b.Beatmaps.Any(bi => bi.RulesetID == 0)).Beatmaps.Last();
+
+ Beatmap.Value = manager.GetWorkingBeatmap(target);
+ Ruleset.Value = rulesets.AvailableRulesets.First(r => r.ID == 0);
+ });
+
+ AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.Equals(target));
+
+ AddUntilStep("has correct ruleset", () => Ruleset.Value.ID == 0);
+
+ // this is an important check, to make sure updateComponentFromBeatmap() was actually run
+ AddUntilStep("selection shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap.BeatmapInfo == target);
+ }
+
[Test]
public void TestRulesetChangeResetsMods()
{
diff --git a/osu.Game.Tests/Visual/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/TestSceneOsuGame.cs
index 492494ada3..8793d880e3 100644
--- a/osu.Game.Tests/Visual/TestSceneOsuGame.cs
+++ b/osu.Game.Tests/Visual/TestSceneOsuGame.cs
@@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual
typeof(OnScreenDisplay),
typeof(NotificationOverlay),
typeof(DirectOverlay),
- typeof(SocialOverlay),
+ typeof(DashboardOverlay),
typeof(ChannelManager),
typeof(ChatOverlay),
typeof(SettingsOverlay),
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
index 39a0e6f6d4..a1822a1163 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
@@ -5,7 +5,7 @@ using System;
namespace osu.Game.Beatmaps.ControlPoints
{
- public abstract class ControlPoint : IComparable, IEquatable
+ public abstract class ControlPoint : IComparable
{
///
/// The time at which the control point takes effect.
@@ -19,12 +19,10 @@ namespace osu.Game.Beatmaps.ControlPoints
public int CompareTo(ControlPoint other) => Time.CompareTo(other.Time);
///
- /// Whether this control point is equivalent to another, ignoring time.
+ /// Determines whether this results in a meaningful change when placed alongside another.
///
- /// Another control point to compare with.
- /// Whether equivalent.
- public abstract bool EquivalentTo(ControlPoint other);
-
- public bool Equals(ControlPoint other) => Time == other?.Time && EquivalentTo(other);
+ /// An existing control point to compare with.
+ /// Whether this is redundant when placed alongside .
+ public abstract bool IsRedundant(ControlPoint existing);
}
}
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
index df68d8acd2..d33a922a32 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
@@ -247,7 +247,7 @@ namespace osu.Game.Beatmaps.ControlPoints
break;
}
- return existing?.EquivalentTo(newPoint) == true;
+ return newPoint?.IsRedundant(existing) == true;
}
private void groupItemAdded(ControlPoint controlPoint)
diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
index 8b21098a51..2448b2b25c 100644
--- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
@@ -27,7 +27,8 @@ namespace osu.Game.Beatmaps.ControlPoints
set => SpeedMultiplierBindable.Value = value;
}
- public override bool EquivalentTo(ControlPoint other) =>
- other is DifficultyControlPoint otherTyped && otherTyped.SpeedMultiplier.Equals(SpeedMultiplier);
+ public override bool IsRedundant(ControlPoint existing)
+ => existing is DifficultyControlPoint existingDifficulty
+ && SpeedMultiplier == existingDifficulty.SpeedMultiplier;
}
}
diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
index 369b93ff3d..9b69147468 100644
--- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs
@@ -35,8 +35,10 @@ namespace osu.Game.Beatmaps.ControlPoints
set => KiaiModeBindable.Value = value;
}
- public override bool EquivalentTo(ControlPoint other) =>
- other is EffectControlPoint otherTyped &&
- KiaiMode == otherTyped.KiaiMode && OmitFirstBarLine == otherTyped.OmitFirstBarLine;
+ public override bool IsRedundant(ControlPoint existing)
+ => !OmitFirstBarLine
+ && existing is EffectControlPoint existingEffect
+ && KiaiMode == existingEffect.KiaiMode
+ && OmitFirstBarLine == existingEffect.OmitFirstBarLine;
}
}
diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
index 393bcfdb3c..61851a00d7 100644
--- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs
@@ -68,8 +68,9 @@ namespace osu.Game.Beatmaps.ControlPoints
return newSampleInfo;
}
- public override bool EquivalentTo(ControlPoint other) =>
- other is SampleControlPoint otherTyped &&
- SampleBank == otherTyped.SampleBank && SampleVolume == otherTyped.SampleVolume;
+ public override bool IsRedundant(ControlPoint existing)
+ => existing is SampleControlPoint existingSample
+ && SampleBank == existingSample.SampleBank
+ && SampleVolume == existingSample.SampleVolume;
}
}
diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
index 51b3377394..1927dd6575 100644
--- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs
@@ -48,8 +48,7 @@ namespace osu.Game.Beatmaps.ControlPoints
///
public double BPM => 60000 / BeatLength;
- public override bool EquivalentTo(ControlPoint other) =>
- other is TimingControlPoint otherTyped
- && TimeSignature == otherTyped.TimeSignature && BeatLength.Equals(otherTyped.BeatLength);
+ // Timing points are never redundant as they can change the time signature.
+ public override bool IsRedundant(ControlPoint existing) => false;
}
}
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
index 12f2c58e35..fe63eec3f9 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
@@ -187,27 +187,13 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine("[HitObjects]");
+ // TODO: implement other legacy rulesets
switch (beatmap.BeatmapInfo.RulesetID)
{
case 0:
foreach (var h in beatmap.HitObjects)
handleOsuHitObject(writer, h);
break;
-
- case 1:
- foreach (var h in beatmap.HitObjects)
- handleTaikoHitObject(writer, h);
- break;
-
- case 2:
- foreach (var h in beatmap.HitObjects)
- handleCatchHitObject(writer, h);
- break;
-
- case 3:
- foreach (var h in beatmap.HitObjects)
- handleManiaHitObject(writer, h);
- break;
}
}
@@ -328,12 +314,6 @@ namespace osu.Game.Beatmaps.Formats
}
}
- private void handleTaikoHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException();
-
- private void handleCatchHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException();
-
- private void handleManiaHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException();
-
private string getSampleBank(IList samples, bool banksOnly = false, bool zeroBanks = false)
{
LegacySampleBank normalBank = toLegacySampleBank(samples.SingleOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank);
diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index 5b2b213322..6406bd88a5 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -150,7 +150,8 @@ namespace osu.Game.Beatmaps.Formats
HitObjects,
Variables,
Fonts,
- Mania
+ CatchTheBeat,
+ Mania,
}
internal class LegacyDifficultyControlPoint : DifficultyControlPoint
@@ -178,9 +179,10 @@ namespace osu.Game.Beatmaps.Formats
return baseInfo;
}
- public override bool EquivalentTo(ControlPoint other) =>
- base.EquivalentTo(other) && other is LegacySampleControlPoint otherTyped &&
- CustomSampleBank == otherTyped.CustomSampleBank;
+ public override bool IsRedundant(ControlPoint existing)
+ => base.IsRedundant(existing)
+ && existing is LegacySampleControlPoint existingSample
+ && CustomSampleBank == existingSample.CustomSampleBank;
}
}
}
diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs
index f36079682e..5a613d1a54 100644
--- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs
+++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs
@@ -103,7 +103,7 @@ namespace osu.Game.Graphics.Containers
TimeSinceLastBeat = beatLength - TimeUntilNextBeat;
- if (timingPoint.Equals(lastTimingPoint) && beatIndex == lastBeat)
+ if (timingPoint == lastTimingPoint && beatIndex == lastBeat)
return;
using (BeginDelayedSequence(-TimeSinceLastBeat, true))
diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs
index e83d899469..94edc33099 100644
--- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs
+++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs
@@ -62,6 +62,14 @@ namespace osu.Game.Input.Bindings
store.KeyBindingChanged -= ReloadMappings;
}
- protected override void ReloadMappings() => KeyBindings = store.Query(ruleset?.ID, variant).ToList();
+ protected override void ReloadMappings()
+ {
+ if (ruleset != null && !ruleset.ID.HasValue)
+ // if the provided ruleset is not stored to the database, we have no way to retrieve custom bindings.
+ // fallback to defaults instead.
+ KeyBindings = DefaultKeyBindings;
+ else
+ KeyBindings = store.Query(ruleset?.ID, variant).ToList();
+ }
}
}
diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs
index 47600e4f68..0bba04cac3 100644
--- a/osu.Game/Online/API/APIRequest.cs
+++ b/osu.Game/Online/API/APIRequest.cs
@@ -18,24 +18,32 @@ namespace osu.Game.Online.API
public T Result { get; private set; }
- protected APIRequest()
- {
- base.Success += () => TriggerSuccess(((OsuJsonWebRequest)WebRequest)?.ResponseObject);
- }
-
///
/// Invoked on successful completion of an API request.
/// This will be scheduled to the API's internal scheduler (run on update thread automatically).
///
public new event APISuccessHandler Success;
+ protected override void PostProcess()
+ {
+ base.PostProcess();
+ Result = ((OsuJsonWebRequest)WebRequest)?.ResponseObject;
+ }
+
internal void TriggerSuccess(T result)
{
if (Result != null)
throw new InvalidOperationException("Attempted to trigger success more than once");
Result = result;
- Success?.Invoke(result);
+
+ TriggerSuccess();
+ }
+
+ internal override void TriggerSuccess()
+ {
+ base.TriggerSuccess();
+ Success?.Invoke(Result);
}
}
@@ -99,6 +107,8 @@ namespace osu.Game.Online.API
if (checkAndScheduleFailure())
return;
+ PostProcess();
+
API.Schedule(delegate
{
if (cancelled) return;
@@ -107,7 +117,14 @@ namespace osu.Game.Online.API
});
}
- internal void TriggerSuccess()
+ ///
+ /// Perform any post-processing actions after a successful request.
+ ///
+ protected virtual void PostProcess()
+ {
+ }
+
+ internal virtual void TriggerSuccess()
{
Success?.Invoke();
}
diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs
index 2c37216fd6..822f628dd2 100644
--- a/osu.Game/Online/Chat/ChannelManager.cs
+++ b/osu.Game/Online/Chat/ChannelManager.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Online.Chat
///
/// Manages everything channel related
///
- public class ChannelManager : PollingComponent
+ public class ChannelManager : PollingComponent, IChannelPostTarget
{
///
/// The channels the player joins on startup
@@ -204,6 +204,10 @@ namespace osu.Game.Online.Chat
switch (command)
{
+ case "np":
+ AddInternal(new NowPlayingCommand());
+ break;
+
case "me":
if (string.IsNullOrWhiteSpace(content))
{
@@ -234,7 +238,7 @@ namespace osu.Game.Online.Chat
break;
case "help":
- target.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action], /join [channel]"));
+ target.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action], /join [channel], /np"));
break;
default:
diff --git a/osu.Game/Online/Chat/IChannelPostTarget.cs b/osu.Game/Online/Chat/IChannelPostTarget.cs
new file mode 100644
index 0000000000..5697e918f0
--- /dev/null
+++ b/osu.Game/Online/Chat/IChannelPostTarget.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.Framework.Allocation;
+
+namespace osu.Game.Online.Chat
+{
+ [Cached(typeof(IChannelPostTarget))]
+ public interface IChannelPostTarget
+ {
+ ///
+ /// Posts a message to the currently opened channel.
+ ///
+ /// The message text that is going to be posted
+ /// Is true if the message is an action, e.g.: user is currently eating
+ /// An optional target channel. If null, will be used.
+ void PostMessage(string text, bool isAction = false, Channel target = null);
+ }
+}
diff --git a/osu.Game/Online/Chat/NowPlayingCommand.cs b/osu.Game/Online/Chat/NowPlayingCommand.cs
new file mode 100644
index 0000000000..c0b54812b6
--- /dev/null
+++ b/osu.Game/Online/Chat/NowPlayingCommand.cs
@@ -0,0 +1,55 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Game.Beatmaps;
+using osu.Game.Online.API;
+using osu.Game.Users;
+
+namespace osu.Game.Online.Chat
+{
+ public class NowPlayingCommand : Component
+ {
+ [Resolved]
+ private IChannelPostTarget channelManager { get; set; }
+
+ [Resolved]
+ private IAPIProvider api { get; set; }
+
+ [Resolved]
+ private Bindable currentBeatmap { get; set; }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ string verb;
+ BeatmapInfo beatmap;
+
+ switch (api.Activity.Value)
+ {
+ case UserActivity.SoloGame solo:
+ verb = "playing";
+ beatmap = solo.Beatmap;
+ break;
+
+ case UserActivity.Editing edit:
+ verb = "editing";
+ beatmap = edit.Beatmap;
+ break;
+
+ default:
+ verb = "listening to";
+ beatmap = currentBeatmap.Value.BeatmapInfo;
+ break;
+ }
+
+ var beatmapString = beatmap.OnlineBeatmapID.HasValue ? $"[https://osu.ppy.sh/b/{beatmap.OnlineBeatmapID} {beatmap}]" : beatmap.ToString();
+
+ channelManager.PostMessage($"is {verb} {beatmapString}", true);
+ Expire();
+ }
+ }
+}
diff --git a/osu.Game/Online/PollingComponent.cs b/osu.Game/Online/PollingComponent.cs
index acbb2c39f4..228f147835 100644
--- a/osu.Game/Online/PollingComponent.cs
+++ b/osu.Game/Online/PollingComponent.cs
@@ -3,7 +3,7 @@
using System;
using System.Threading.Tasks;
-using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Framework.Threading;
namespace osu.Game.Online
@@ -11,7 +11,7 @@ namespace osu.Game.Online
///
/// A component which requires a constant polling process.
///
- public abstract class PollingComponent : Component
+ public abstract class PollingComponent : CompositeDrawable // switch away from Component because InternalChildren are used in usages.
{
private double? lastTimePolled;
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 5e93d760e3..c861b84835 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -67,7 +67,7 @@ namespace osu.Game
private DirectOverlay direct;
- private SocialOverlay social;
+ private DashboardOverlay dashboard;
private UserProfileOverlay userProfile;
@@ -611,7 +611,7 @@ namespace osu.Game
//overlay elements
loadComponentSingleFile(direct = new DirectOverlay(), overlayContent.Add, true);
- loadComponentSingleFile(social = new SocialOverlay(), overlayContent.Add, true);
+ loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true);
var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true);
loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true);
loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true);
@@ -670,7 +670,7 @@ namespace osu.Game
}
// ensure only one of these overlays are open at once.
- var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, social, direct, changelogOverlay, rankingsOverlay };
+ var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, dashboard, direct, changelogOverlay, rankingsOverlay };
foreach (var overlay in singleDisplayOverlays)
{
@@ -842,7 +842,7 @@ namespace osu.Game
return true;
case GlobalAction.ToggleSocial:
- social.ToggleVisibility();
+ dashboard.ToggleVisibility();
return true;
case GlobalAction.ResetInputSettings:
diff --git a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs
new file mode 100644
index 0000000000..9ee679a866
--- /dev/null
+++ b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.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.
+
+namespace osu.Game.Overlays.Dashboard
+{
+ public class DashboardOverlayHeader : TabControlOverlayHeader
+ {
+ protected override OverlayTitle CreateTitle() => new DashboardTitle();
+
+ private class DashboardTitle : OverlayTitle
+ {
+ public DashboardTitle()
+ {
+ Title = "dashboard";
+ IconTexture = "Icons/changelog";
+ }
+ }
+ }
+
+ public enum DashboardOverlayTabs
+ {
+ Friends
+ }
+}
diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs
index 3c9b31daae..79fda99c73 100644
--- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs
+++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs
@@ -16,7 +16,7 @@ using osuTK;
namespace osu.Game.Overlays.Dashboard.Friends
{
- public class FriendDisplay : CompositeDrawable
+ public class FriendDisplay : OverlayView>
{
private List users = new List();
@@ -26,34 +26,29 @@ namespace osu.Game.Overlays.Dashboard.Friends
set
{
users = value;
-
onlineStreamControl.Populate(value);
}
}
- [Resolved]
- private IAPIProvider api { get; set; }
-
- private GetFriendsRequest request;
private CancellationTokenSource cancellationToken;
private Drawable currentContent;
- private readonly FriendOnlineStreamControl onlineStreamControl;
- private readonly Box background;
- private readonly Box controlBackground;
- private readonly UserListToolbar userListToolbar;
- private readonly Container itemsPlaceholder;
- private readonly LoadingLayer loading;
+ private FriendOnlineStreamControl onlineStreamControl;
+ private Box background;
+ private Box controlBackground;
+ private UserListToolbar userListToolbar;
+ private Container itemsPlaceholder;
+ private LoadingLayer loading;
- public FriendDisplay()
+ [BackgroundDependencyLoader]
+ private void load(OverlayColourProvider colourProvider)
{
- RelativeSizeAxes = Axes.X;
- AutoSizeAxes = Axes.Y;
InternalChild = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new Container
@@ -134,11 +129,7 @@ namespace osu.Game.Overlays.Dashboard.Friends
}
}
};
- }
- [BackgroundDependencyLoader]
- private void load(OverlayColourProvider colourProvider)
- {
background.Colour = colourProvider.Background4;
controlBackground.Colour = colourProvider.Background5;
}
@@ -152,14 +143,11 @@ namespace osu.Game.Overlays.Dashboard.Friends
userListToolbar.SortCriteria.BindValueChanged(_ => recreatePanels());
}
- public void Fetch()
- {
- if (!api.IsLoggedIn)
- return;
+ protected override APIRequest> CreateRequest() => new GetFriendsRequest();
- request = new GetFriendsRequest();
- request.Success += response => Schedule(() => Users = response);
- api.Queue(request);
+ protected override void OnSuccess(List response)
+ {
+ Users = response;
}
private void recreatePanels()
@@ -258,9 +246,7 @@ namespace osu.Game.Overlays.Dashboard.Friends
protected override void Dispose(bool isDisposing)
{
- request?.Cancel();
cancellationToken?.Cancel();
-
base.Dispose(isDisposing);
}
}
diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs
new file mode 100644
index 0000000000..a72c3f4fa5
--- /dev/null
+++ b/osu.Game/Overlays/DashboardOverlay.cs
@@ -0,0 +1,150 @@
+// 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.Threading;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.API;
+using osu.Game.Overlays.Dashboard;
+using osu.Game.Overlays.Dashboard.Friends;
+
+namespace osu.Game.Overlays
+{
+ public class DashboardOverlay : FullscreenOverlay
+ {
+ private CancellationTokenSource cancellationToken;
+
+ private Box background;
+ private Container content;
+ private DashboardOverlayHeader header;
+ private LoadingLayer loading;
+ private OverlayScrollContainer scrollFlow;
+
+ public DashboardOverlay()
+ : base(OverlayColourScheme.Purple)
+ {
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Children = new Drawable[]
+ {
+ background = new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ scrollFlow = new OverlayScrollContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ ScrollbarVisible = false,
+ Child = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Y,
+ RelativeSizeAxes = Axes.X,
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ header = new DashboardOverlayHeader
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Depth = -float.MaxValue
+ },
+ content = new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y
+ }
+ }
+ }
+ },
+ loading = new LoadingLayer(content),
+ };
+
+ background.Colour = ColourProvider.Background5;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ header.Current.BindValueChanged(onTabChanged);
+ }
+
+ private bool displayUpdateRequired = true;
+
+ protected override void PopIn()
+ {
+ base.PopIn();
+
+ // We don't want to create a new display on every call, only when exiting from fully closed state.
+ if (displayUpdateRequired)
+ {
+ header.Current.TriggerChange();
+ displayUpdateRequired = false;
+ }
+ }
+
+ protected override void PopOutComplete()
+ {
+ base.PopOutComplete();
+ loadDisplay(Empty());
+ displayUpdateRequired = true;
+ }
+
+ private void loadDisplay(Drawable display)
+ {
+ scrollFlow.ScrollToStart();
+
+ LoadComponentAsync(display, loaded =>
+ {
+ if (API.IsLoggedIn)
+ loading.Hide();
+
+ content.Child = loaded;
+ }, (cancellationToken = new CancellationTokenSource()).Token);
+ }
+
+ private void onTabChanged(ValueChangedEvent tab)
+ {
+ cancellationToken?.Cancel();
+ loading.Show();
+
+ if (!API.IsLoggedIn)
+ {
+ loadDisplay(Empty());
+ return;
+ }
+
+ switch (tab.NewValue)
+ {
+ case DashboardOverlayTabs.Friends:
+ loadDisplay(new FriendDisplay());
+ break;
+
+ default:
+ throw new NotImplementedException($"Display for {tab.NewValue} tab is not implemented");
+ }
+ }
+
+ public override void APIStateChanged(IAPIProvider api, APIState state)
+ {
+ if (State.Value == Visibility.Hidden)
+ return;
+
+ header.Current.TriggerChange();
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ cancellationToken?.Cancel();
+ base.Dispose(isDisposing);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/OverlayHeader.cs b/osu.Game/Overlays/OverlayHeader.cs
index 4ac0f697c3..dbc934bde9 100644
--- a/osu.Game/Overlays/OverlayHeader.cs
+++ b/osu.Game/Overlays/OverlayHeader.cs
@@ -12,6 +12,8 @@ namespace osu.Game.Overlays
{
public abstract class OverlayHeader : Container
{
+ public const int CONTENT_X_MARGIN = 50;
+
private readonly Box titleBackground;
protected readonly FillFlowContainer HeaderInfo;
@@ -54,7 +56,7 @@ namespace osu.Game.Overlays
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding
{
- Horizontal = UserProfileOverlay.CONTENT_X_MARGIN,
+ Horizontal = CONTENT_X_MARGIN,
},
Children = new[]
{
diff --git a/osu.Game/Overlays/OverlayView.cs b/osu.Game/Overlays/OverlayView.cs
new file mode 100644
index 0000000000..3e2c54c726
--- /dev/null
+++ b/osu.Game/Overlays/OverlayView.cs
@@ -0,0 +1,79 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Online.API;
+
+namespace osu.Game.Overlays
+{
+ ///
+ /// A subview containing online content, to be displayed inside a .
+ ///
+ ///
+ /// Automatically performs a data fetch on load.
+ ///
+ /// The type of the API response.
+ public abstract class OverlayView : CompositeDrawable, IOnlineComponent
+ where T : class
+ {
+ [Resolved]
+ protected IAPIProvider API { get; private set; }
+
+ private APIRequest request;
+
+ protected OverlayView()
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ API.Register(this);
+ }
+
+ ///
+ /// Create the API request for fetching data.
+ ///
+ protected abstract APIRequest CreateRequest();
+
+ ///
+ /// Fired when results arrive from the main API request.
+ ///
+ ///
+ protected abstract void OnSuccess(T response);
+
+ ///
+ /// Force a re-request for data from the API.
+ ///
+ protected void PerformFetch()
+ {
+ request?.Cancel();
+
+ request = CreateRequest();
+ request.Success += response => Schedule(() => OnSuccess(response));
+
+ API.Queue(request);
+ }
+
+ public virtual void APIStateChanged(IAPIProvider api, APIState state)
+ {
+ switch (state)
+ {
+ case APIState.Online:
+ PerformFetch();
+ break;
+ }
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ request?.Cancel();
+ API?.Unregister(this);
+ base.Dispose(isDisposing);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/TabControlOverlayHeader.cs b/osu.Game/Overlays/TabControlOverlayHeader.cs
index ab1a6aff78..e8e000f441 100644
--- a/osu.Game/Overlays/TabControlOverlayHeader.cs
+++ b/osu.Game/Overlays/TabControlOverlayHeader.cs
@@ -44,7 +44,7 @@ namespace osu.Game.Overlays
},
TabControl = CreateTabControl().With(control =>
{
- control.Margin = new MarginPadding { Left = UserProfileOverlay.CONTENT_X_MARGIN };
+ control.Margin = new MarginPadding { Left = CONTENT_X_MARGIN };
control.Current = Current;
})
}
diff --git a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs
index 5e353d3319..f6646eb81d 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs
@@ -14,9 +14,9 @@ namespace osu.Game.Overlays.Toolbar
}
[BackgroundDependencyLoader(true)]
- private void load(SocialOverlay chat)
+ private void load(DashboardOverlay dashboard)
{
- StateContainer = chat;
+ StateContainer = dashboard;
}
}
}
diff --git a/osu.Game/Rulesets/Mods/ModHidden.cs b/osu.Game/Rulesets/Mods/ModHidden.cs
index 4e4a75db82..a1915b974c 100644
--- a/osu.Game/Rulesets/Mods/ModHidden.cs
+++ b/osu.Game/Rulesets/Mods/ModHidden.cs
@@ -23,6 +23,13 @@ namespace osu.Game.Rulesets.Mods
protected Bindable IncreaseFirstObjectVisibility = new Bindable();
+ ///
+ /// Check whether the provided hitobject should be considered the "first" hideable object.
+ /// Can be used to skip spinners, for instance.
+ ///
+ /// The hitobject to check.
+ protected virtual bool IsFirstHideableObject(DrawableHitObject hitObject) => true;
+
public void ReadFromConfig(OsuConfigManager config)
{
IncreaseFirstObjectVisibility = config.GetBindable(OsuSetting.IncreaseFirstObjectVisibility);
@@ -30,8 +37,11 @@ namespace osu.Game.Rulesets.Mods
public virtual void ApplyToDrawableHitObjects(IEnumerable drawables)
{
- foreach (var d in drawables.Skip(IncreaseFirstObjectVisibility.Value ? 1 : 0))
- d.ApplyCustomUpdateState += ApplyHiddenState;
+ if (IncreaseFirstObjectVisibility.Value)
+ drawables = drawables.SkipWhile(h => !IsFirstHideableObject(h)).Skip(1);
+
+ foreach (var dho in drawables)
+ dho.ApplyCustomUpdateState += ApplyHiddenState;
}
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index 5bc2e1aa56..478c46fb36 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -426,7 +426,7 @@ namespace osu.Game.Screens.Select
}
///
- /// selection has been changed as the result of a user interaction.
+ /// Selection has been changed as the result of a user interaction.
///
private void performUpdateSelected()
{
@@ -435,7 +435,7 @@ namespace osu.Game.Screens.Select
selectionChangedDebounce?.Cancel();
- if (beatmap == null)
+ if (beatmapNoDebounce == null)
run();
else
selectionChangedDebounce = Scheduler.AddDelayed(run, 200);
@@ -448,9 +448,11 @@ namespace osu.Game.Screens.Select
{
Mods.Value = Array.Empty();
- // required to return once in order to have the carousel in a good state.
- // if the ruleset changed, the rest of the selection update will happen via updateSelectedRuleset.
- return;
+ // transferRulesetValue() may trigger a refilter. If the current selection does not match the new ruleset, we want to switch away from it.
+ // The default logic on WorkingBeatmap change is to switch to a matching ruleset (see workingBeatmapChanged()), but we don't want that here.
+ // We perform an early selection attempt and clear out the beatmap selection to avoid a second ruleset change (revert).
+ if (beatmap != null && !Carousel.SelectBeatmap(beatmap, false))
+ beatmap = null;
}
// We may be arriving here due to another component changing the bindable Beatmap.
@@ -714,7 +716,7 @@ namespace osu.Game.Screens.Select
if (decoupledRuleset.Value?.Equals(Ruleset.Value) == true)
return false;
- Logger.Log($"decoupled ruleset transferred (\"{decoupledRuleset.Value}\" -> \"{Ruleset.Value}\"");
+ Logger.Log($"decoupled ruleset transferred (\"{decoupledRuleset.Value}\" -> \"{Ruleset.Value}\")");
rulesetNoDebounce = decoupledRuleset.Value = Ruleset.Value;
// if we have a pending filter operation, we want to run it now.
diff --git a/osu.Game/Skinning/LegacySkinDecoder.cs b/osu.Game/Skinning/LegacySkinDecoder.cs
index 5d4b8de7ac..75b7ba28b9 100644
--- a/osu.Game/Skinning/LegacySkinDecoder.cs
+++ b/osu.Game/Skinning/LegacySkinDecoder.cs
@@ -44,6 +44,12 @@ namespace osu.Game.Skinning
}
break;
+
+ // osu!catch section only has colour settings
+ // so no harm in handling the entire section
+ case Section.CatchTheBeat:
+ HandleColours(skin, line);
+ return;
}
if (!string.IsNullOrEmpty(pair.Key))
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 76f7a030f9..35ee0864e1 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -19,11 +19,11 @@
-
+
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 7a487a6430..0200fca9a3 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,17 +70,17 @@
-
+
-
+
-
+