diff --git a/osu.Game.Rulesets.Catch/UI/CatchTouchInputMapper.cs b/osu.Game.Rulesets.Catch/UI/CatchTouchInputMapper.cs
index e6736d6c93..12d695393f 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchTouchInputMapper.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchTouchInputMapper.cs
@@ -35,6 +35,8 @@ namespace osu.Game.Rulesets.Catch.UI
private void load(CatchInputManager catchInputManager, OsuColour colours)
{
const float width = 0.15f;
+ // Ratio between normal move area height and total input height
+ const float normal_area_height_ratio = 0.45f;
keyBindingContainer = catchInputManager.KeyBindingContainer;
@@ -54,18 +56,18 @@ namespace osu.Game.Rulesets.Catch.UI
Width = width,
Children = new Drawable[]
{
- leftDashBox = new InputArea(TouchCatchAction.DashLeft, trackedActionSources)
- {
- RelativeSizeAxes = Axes.Both,
- Width = 0.5f,
- },
leftBox = new InputArea(TouchCatchAction.MoveLeft, trackedActionSources)
{
RelativeSizeAxes = Axes.Both,
- Width = 0.5f,
+ Height = normal_area_height_ratio,
Colour = colours.Gray9,
- Anchor = Anchor.TopRight,
- Origin = Anchor.TopRight,
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ },
+ leftDashBox = new InputArea(TouchCatchAction.DashLeft, trackedActionSources)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Height = 1 - normal_area_height_ratio,
},
}
},
@@ -80,15 +82,15 @@ namespace osu.Game.Rulesets.Catch.UI
rightBox = new InputArea(TouchCatchAction.MoveRight, trackedActionSources)
{
RelativeSizeAxes = Axes.Both,
- Width = 0.5f,
+ Height = normal_area_height_ratio,
Colour = colours.Gray9,
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
},
rightDashBox = new InputArea(TouchCatchAction.DashRight, trackedActionSources)
{
RelativeSizeAxes = Axes.Both,
- Width = 0.5f,
- Anchor = Anchor.TopRight,
- Origin = Anchor.TopRight,
+ Height = 1 - normal_area_height_ratio,
},
}
},
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
index 0296303867..0cba2076be 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
@@ -23,6 +23,15 @@ using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests
{
+ ///
+ /// Diagrams in this class are represented as:
+ /// - : time
+ /// O : note
+ /// [ ] : hold note
+ ///
+ /// x : button press
+ /// o : button release
+ ///
public class TestSceneHoldNoteInput : RateAdjustedBeatmapTestScene
{
private const double time_before_head = 250;
@@ -223,6 +232,149 @@ namespace osu.Game.Rulesets.Mania.Tests
assertTailJudgement(HitResult.Meh);
}
+ ///
+ /// -----[ ]-O-------------
+ /// xo o
+ ///
+ [Test]
+ public void TestPressAndReleaseJustBeforeTailWithNearbyNoteAndCloseByHead()
+ {
+ Note note;
+
+ const int duration = 50;
+
+ var beatmap = new Beatmap
+ {
+ HitObjects =
+ {
+ // hold note is very short, to make the head still in range
+ new HoldNote
+ {
+ StartTime = time_head,
+ Duration = duration,
+ Column = 0,
+ },
+ {
+ // Next note within tail lenience
+ note = new Note
+ {
+ StartTime = time_head + duration + 10
+ }
+ }
+ },
+ BeatmapInfo =
+ {
+ Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
+ Ruleset = new ManiaRuleset().RulesetInfo
+ },
+ };
+
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_head + duration, ManiaAction.Key1),
+ new ManiaReplayFrame(time_head + duration + 10),
+ }, beatmap);
+
+ assertHeadJudgement(HitResult.Good);
+ assertTailJudgement(HitResult.Perfect);
+
+ assertHitObjectJudgement(note, HitResult.Miss);
+ }
+
+ ///
+ /// -----[ ]--O--
+ /// xo o
+ ///
+ [Test]
+ public void TestPressAndReleaseJustBeforeTailWithNearbyNote()
+ {
+ Note note;
+
+ var beatmap = new Beatmap
+ {
+ HitObjects =
+ {
+ new HoldNote
+ {
+ StartTime = time_head,
+ Duration = time_tail - time_head,
+ Column = 0,
+ },
+ {
+ // Next note within tail lenience
+ note = new Note
+ {
+ StartTime = time_tail + 50
+ }
+ }
+ },
+ BeatmapInfo =
+ {
+ Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
+ Ruleset = new ManiaRuleset().RulesetInfo
+ },
+ };
+
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_tail - 10, ManiaAction.Key1),
+ new ManiaReplayFrame(time_tail),
+ }, beatmap);
+
+ assertHeadJudgement(HitResult.Miss);
+ assertTickJudgement(HitResult.LargeTickMiss);
+ assertTailJudgement(HitResult.Miss);
+
+ assertHitObjectJudgement(note, HitResult.Good);
+ }
+
+ ///
+ /// -----[ ]--O--
+ /// xo o
+ ///
+ [Test]
+ public void TestPressAndReleaseJustAfterTailWithNearbyNote()
+ {
+ Note note;
+
+ var beatmap = new Beatmap
+ {
+ HitObjects =
+ {
+ new HoldNote
+ {
+ StartTime = time_head,
+ Duration = time_tail - time_head,
+ Column = 0,
+ },
+ {
+ // Next note within tail lenience
+ note = new Note
+ {
+ StartTime = time_tail + 50
+ }
+ }
+ },
+ BeatmapInfo =
+ {
+ Difficulty = new BeatmapDifficulty { SliderTickRate = 4 },
+ Ruleset = new ManiaRuleset().RulesetInfo
+ },
+ };
+
+ performTest(new List
+ {
+ new ManiaReplayFrame(time_tail + 10, ManiaAction.Key1),
+ new ManiaReplayFrame(time_tail + 20),
+ }, beatmap);
+
+ assertHeadJudgement(HitResult.Miss);
+ assertTickJudgement(HitResult.LargeTickMiss);
+ assertTailJudgement(HitResult.Miss);
+
+ assertHitObjectJudgement(note, HitResult.Great);
+ }
+
///
/// -----[ ]-----
/// xo o
@@ -351,20 +503,23 @@ namespace osu.Game.Rulesets.Mania.Tests
.All(j => j.Type.IsHit()));
}
+ private void assertHitObjectJudgement(HitObject hitObject, HitResult result)
+ => AddAssert($"object judged as {result}", () => judgementResults.First(j => j.HitObject == hitObject).Type, () => Is.EqualTo(result));
+
private void assertHeadJudgement(HitResult result)
- => AddAssert($"head judged as {result}", () => judgementResults.First(j => j.HitObject is Note).Type == result);
+ => AddAssert($"head judged as {result}", () => judgementResults.First(j => j.HitObject is Note).Type, () => Is.EqualTo(result));
private void assertTailJudgement(HitResult result)
- => AddAssert($"tail judged as {result}", () => judgementResults.Single(j => j.HitObject is TailNote).Type == result);
+ => AddAssert($"tail judged as {result}", () => judgementResults.Single(j => j.HitObject is TailNote).Type, () => Is.EqualTo(result));
private void assertNoteJudgement(HitResult result)
- => AddAssert($"hold note judged as {result}", () => judgementResults.Single(j => j.HitObject is HoldNote).Type == result);
+ => AddAssert($"hold note judged as {result}", () => judgementResults.Single(j => j.HitObject is HoldNote).Type, () => Is.EqualTo(result));
private void assertTickJudgement(HitResult result)
- => AddAssert($"any tick judged as {result}", () => judgementResults.Where(j => j.HitObject is HoldNoteTick).Any(j => j.Type == result));
+ => AddAssert($"any tick judged as {result}", () => judgementResults.Where(j => j.HitObject is HoldNoteTick).Select(j => j.Type), () => Does.Contain(result));
private void assertLastTickJudgement(HitResult result)
- => AddAssert($"last tick judged as {result}", () => judgementResults.Last(j => j.HitObject is HoldNoteTick).Type == result);
+ => AddAssert($"last tick judged as {result}", () => judgementResults.Last(j => j.HitObject is HoldNoteTick).Type, () => Is.EqualTo(result));
private ScoreAccessibleReplayPlayer currentPlayer;
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index c68eec610c..86d4ad0a36 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -262,14 +262,24 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
tick.MissForcefully();
}
- ApplyResult(r => r.Type = Tail.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult);
- endHold();
+ if (Tail.IsHit)
+ ApplyResult(r => r.Type = r.Judgement.MaxResult);
+ else
+ MissForcefully();
}
if (Tail.Judged && !Tail.IsHit)
HoldBrokenTime = Time.Current;
}
+ public override void MissForcefully()
+ {
+ base.MissForcefully();
+
+ // Important that this is always called when a result is applied.
+ endHold();
+ }
+
public bool OnPressed(KeyBindingPressEvent e)
{
if (AllJudged)
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
index 73dc937a00..51f8ab1cf8 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs
@@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
///
/// Causes this to get missed, disregarding all conditions in implementations of .
///
- public void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult);
+ public virtual void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult);
}
public abstract class DrawableManiaHitObject : DrawableManiaHitObject
diff --git a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteTailPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteTailPiece.cs
index e1068c6cd8..00cd37b6cf 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteTailPiece.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Argon/ArgonHoldNoteTailPiece.cs
@@ -38,6 +38,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
},
new Container
{
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both,
Height = 0.82f,
Masking = true,
@@ -54,6 +56,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
{
RelativeSizeAxes = Axes.X,
Height = ArgonNotePiece.CORNER_RADIUS * 2,
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
},
};
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs
index e42dc254ac..25cb3f7886 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs
@@ -26,8 +26,10 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
};
[Test]
- public void DrumrollTest()
+ public void TestDrumroll([Values] bool withKiai)
{
+ AddStep("set up beatmap", () => setUpBeatmap(withKiai));
+
AddStep("Drum roll", () => SetContents(_ =>
{
var hoc = new ScrollingHitObjectContainer();
@@ -73,5 +75,22 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
return drumroll;
}
+
+ private void setUpBeatmap(bool withKiai)
+ {
+ var controlPointInfo = new ControlPointInfo();
+
+ controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
+
+ if (withKiai)
+ controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
+
+ Beatmap.Value = CreateWorkingBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPointInfo
+ });
+
+ Beatmap.Value.Track.Start();
+ }
}
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRollKiai.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRollKiai.cs
deleted file mode 100644
index 53977150e7..0000000000
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRollKiai.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using NUnit.Framework;
-using osu.Game.Beatmaps.ControlPoints;
-using osu.Game.Beatmaps;
-
-namespace osu.Game.Rulesets.Taiko.Tests.Skinning
-{
- [TestFixture]
- public class TestSceneDrawableDrumRollKiai : TestSceneDrawableDrumRoll
- {
- [SetUp]
- public void SetUp() => Schedule(() =>
- {
- var controlPointInfo = new ControlPointInfo();
-
- controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
- controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
-
- Beatmap.Value = CreateWorkingBeatmap(new Beatmap
- {
- ControlPointInfo = controlPointInfo
- });
-
- // track needs to be playing for BeatSyncedContainer to work.
- Beatmap.Value.Track.Start();
- });
- }
-}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs
index eb2b6c1d74..adfd27c5d6 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs
@@ -4,19 +4,51 @@
#nullable disable
using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
+using osu.Game.Screens.Play;
+using osu.Game.Tests.Gameplay;
namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
[TestFixture]
public class TestSceneDrawableHit : TaikoSkinnableTestScene
{
+ [Cached]
+ private GameplayState gameplayState = TestGameplayState.Create(new TaikoRuleset());
+
[Test]
- public void TestHits()
+ public void TestHits([Values] bool withKiai)
+ {
+ AddStep("Create beatmap", () => setUpBeatmap(withKiai));
+ addHitSteps();
+ }
+
+ [Test]
+ public void TestHitAnimationSlow()
+ {
+ AddStep("Create beatmap", () => setUpBeatmap(false));
+
+ AddStep("Set 50 combo", () => gameplayState.ScoreProcessor.Combo.Value = 50);
+ addHitSteps();
+ AddStep("Reset combo", () => gameplayState.ScoreProcessor.Combo.Value = 0);
+ }
+
+ [Test]
+ public void TestHitAnimationFast()
+ {
+ AddStep("Create beatmap", () => setUpBeatmap(false));
+
+ AddStep("Set 150 combo", () => gameplayState.ScoreProcessor.Combo.Value = 150);
+ addHitSteps();
+ AddStep("Reset combo", () => gameplayState.ScoreProcessor.Combo.Value = 0);
+ }
+
+ private void addHitSteps()
{
AddStep("Centre hit", () => SetContents(_ => new DrawableHit(createHitAtCurrentTime())
{
@@ -56,5 +88,22 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
return hit;
}
+
+ private void setUpBeatmap(bool withKiai)
+ {
+ var controlPointInfo = new ControlPointInfo();
+
+ controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
+
+ if (withKiai)
+ controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
+
+ Beatmap.Value = CreateWorkingBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPointInfo
+ });
+
+ Beatmap.Value.Track.Start();
+ }
}
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHitKiai.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHitKiai.cs
deleted file mode 100644
index fac0530749..0000000000
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHitKiai.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using NUnit.Framework;
-using osu.Game.Beatmaps;
-using osu.Game.Beatmaps.ControlPoints;
-
-namespace osu.Game.Rulesets.Taiko.Tests.Skinning
-{
- [TestFixture]
- public class TestSceneDrawableHitKiai : TestSceneDrawableHit
- {
- [SetUp]
- public void SetUp() => Schedule(() =>
- {
- var controlPointInfo = new ControlPointInfo();
-
- controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
- controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
-
- Beatmap.Value = CreateWorkingBeatmap(new Beatmap
- {
- ControlPointInfo = controlPointInfo
- });
-
- // track needs to be playing for BeatSyncedContainer to work.
- Beatmap.Value.Track.Start();
- });
- }
-}
diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
index 4a2b97a4cb..ff187a133a 100644
--- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
+++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
@@ -6,10 +6,10 @@
using System;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
-using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Utils;
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
{
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs
index 6b2576a564..a5867ff51c 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs
@@ -2,13 +2,16 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
+using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Screens.Play;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
@@ -18,6 +21,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
public class LegacyCirclePiece : CompositeDrawable, IHasAccentColour
{
private Drawable backgroundLayer = null!;
+ private Drawable? foregroundLayer;
+
+ private Bindable currentCombo { get; } = new BindableInt();
+
+ private int animationFrame;
+ private double beatLength;
// required for editor blueprints (not sure why these circle pieces are zero size).
public override Quad ScreenSpaceDrawQuad => backgroundLayer.ScreenSpaceDrawQuad;
@@ -27,6 +36,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
RelativeSizeAxes = Axes.Both;
}
+ [Resolved(canBeNull: true)]
+ private GameplayState? gameplayState { get; set; }
+
+ [Resolved(canBeNull: true)]
+ private IBeatSyncProvider? beatSyncProvider { get; set; }
+
[BackgroundDependencyLoader]
private void load(ISkinSource skin, DrawableHitObject drawableHitObject)
{
@@ -45,7 +60,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
// backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer.
AddInternal(backgroundLayer = new LegacyKiaiFlashingDrawable(() => getDrawableFor("circle")));
- var foregroundLayer = getDrawableFor("circleoverlay");
+ foregroundLayer = getDrawableFor("circleoverlay");
if (foregroundLayer != null)
AddInternal(foregroundLayer);
@@ -58,6 +73,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
c.Anchor = Anchor.Centre;
c.Origin = Anchor.Centre;
}
+
+ if (gameplayState != null)
+ currentCombo.BindTo(gameplayState.ScoreProcessor.Combo);
}
protected override void LoadComplete()
@@ -74,6 +92,37 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
// This ensures they are scaled relative to each other but also match the expected DrawableHit size.
foreach (var c in InternalChildren)
c.Scale = new Vector2(DrawHeight / 128);
+
+ if (foregroundLayer is IFramedAnimation animatableForegroundLayer)
+ animateForegroundLayer(animatableForegroundLayer);
+ }
+
+ private void animateForegroundLayer(IFramedAnimation animatableForegroundLayer)
+ {
+ int multiplier;
+
+ if (currentCombo.Value >= 150)
+ {
+ multiplier = 2;
+ }
+ else if (currentCombo.Value >= 50)
+ {
+ multiplier = 1;
+ }
+ else
+ {
+ animatableForegroundLayer.GotoFrame(0);
+ return;
+ }
+
+ if (beatSyncProvider?.ControlPoints != null)
+ {
+ beatLength = beatSyncProvider.ControlPoints.TimingPointAt(Time.Current).BeatLength;
+
+ animationFrame = Time.Current % ((beatLength * 2) / multiplier) >= beatLength / multiplier ? 0 : 1;
+
+ animatableForegroundLayer.GotoFrame(animationFrame);
+ }
}
private Color4 accentColour;
diff --git a/osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs b/osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs
index 8cd26901c5..8809ce3adc 100644
--- a/osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs
+++ b/osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs
@@ -5,7 +5,7 @@
using System;
using NUnit.Framework;
-using osu.Game.Rulesets.Difficulty.Utils;
+using osu.Game.Utils;
namespace osu.Game.Tests.NonVisual
{
diff --git a/osu.Game.Tests/Visual/Online/TestSceneGraph.cs b/osu.Game.Tests/Visual/Online/TestSceneGraph.cs
index 2f3331b141..8b0536651d 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneGraph.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneGraph.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
@@ -32,6 +33,7 @@ namespace osu.Game.Tests.Visual.Online
AddStep("values from 1-10", () => graph.Values = Enumerable.Range(1, 10).Select(i => (float)i));
AddStep("values from 1-100", () => graph.Values = Enumerable.Range(1, 100).Select(i => (float)i));
AddStep("reversed values from 1-10", () => graph.Values = Enumerable.Range(1, 10).Reverse().Select(i => (float)i));
+ AddStep("empty values", () => graph.Values = Array.Empty());
AddStep("Bottom to top", () => graph.Direction = BarDirection.BottomToTop);
AddStep("Top to bottom", () => graph.Direction = BarDirection.TopToBottom);
AddStep("Left to right", () => graph.Direction = BarDirection.LeftToRight);
diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankGraph.cs b/osu.Game.Tests/Visual/Online/TestSceneRankGraph.cs
index 1b1a5c7c6a..d05f1f02f7 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneRankGraph.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneRankGraph.cs
@@ -1,14 +1,15 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Testing;
using osu.Game.Graphics;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Profile.Header.Components;
@@ -23,33 +24,14 @@ namespace osu.Game.Tests.Visual.Online
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
- public TestSceneRankGraph()
+ private RankGraph graph = null!;
+
+ private const int history_length = 89;
+
+ [SetUpSteps]
+ public void SetUpSteps()
{
- RankGraph graph;
-
- int[] data = new int[89];
- int[] dataWithZeros = new int[89];
- int[] smallData = new int[89];
- int[] edgyData = new int[89];
-
- for (int i = 0; i < 89; i++)
- data[i] = dataWithZeros[i] = (i + 1) * 1000;
-
- for (int i = 20; i < 60; i++)
- dataWithZeros[i] = 0;
-
- for (int i = 79; i < 89; i++)
- smallData[i] = 100000 - i * 1000;
-
- bool edge = true;
-
- for (int i = 0; i < 20; i++)
- {
- edgyData[i] = 100000 + (edge ? 1000 : -1000) * (i + 1);
- edge = !edge;
- }
-
- Add(new Container
+ AddStep("create graph", () => Child = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -67,34 +49,70 @@ namespace osu.Game.Tests.Visual.Online
}
}
});
+ }
+ [Test]
+ public void TestNullUser()
+ {
AddStep("null user", () => graph.Statistics.Value = null);
+ AddAssert("line graph hidden", () => this.ChildrenOfType().All(graph => graph.Alpha == 0));
+ }
+
+ [Test]
+ public void TestRankOnly()
+ {
AddStep("rank only", () =>
{
graph.Statistics.Value = new UserStatistics
{
+ IsRanked = true,
GlobalRank = 123456,
PP = 12345,
};
});
+ AddAssert("line graph hidden", () => this.ChildrenOfType().All(graph => graph.Alpha == 0));
+ }
+
+ [Test]
+ public void TestWithRankHistory()
+ {
+ int[] data = new int[history_length];
+
+ for (int i = 0; i < history_length; i++)
+ data[i] = (i + 1) * 1000;
AddStep("with rank history", () =>
{
graph.Statistics.Value = new UserStatistics
{
+ IsRanked = true,
GlobalRank = 89000,
PP = 12345,
RankHistory = new APIRankHistory
{
- Data = data,
+ Data = data
}
};
});
+ AddAssert("line graph shown", () => this.ChildrenOfType().All(graph => graph.Alpha == 1));
+ }
+
+ [Test]
+ public void TestRanksWithZeroValues()
+ {
+ int[] dataWithZeros = new int[history_length];
+
+ for (int i = 0; i < history_length; i++)
+ {
+ if (i < 20 || i >= 60)
+ dataWithZeros[i] = (i + 1) * 1000;
+ }
AddStep("with zero values", () =>
{
graph.Statistics.Value = new UserStatistics
{
+ IsRanked = true,
GlobalRank = 89000,
PP = 12345,
RankHistory = new APIRankHistory
@@ -103,11 +121,22 @@ namespace osu.Game.Tests.Visual.Online
}
};
});
+ AddAssert("line graph shown", () => this.ChildrenOfType().All(graph => graph.Alpha == 1));
+ }
+
+ [Test]
+ public void TestSmallAmountOfData()
+ {
+ int[] smallData = new int[history_length];
+
+ for (int i = history_length - 10; i < history_length; i++)
+ smallData[i] = 100000 - i * 1000;
AddStep("small amount of data", () =>
{
graph.Statistics.Value = new UserStatistics
{
+ IsRanked = true,
GlobalRank = 12000,
PP = 12345,
RankHistory = new APIRankHistory
@@ -116,11 +145,27 @@ namespace osu.Game.Tests.Visual.Online
}
};
});
+ AddAssert("line graph shown", () => this.ChildrenOfType().All(graph => graph.Alpha == 1));
+ }
+
+ [Test]
+ public void TestHistoryWithEdges()
+ {
+ int[] edgyData = new int[89];
+
+ bool edge = true;
+
+ for (int i = 0; i < 20; i++)
+ {
+ edgyData[i] = 100000 + (edge ? 1000 : -1000) * (i + 1);
+ edge = !edge;
+ }
AddStep("graph with edges", () =>
{
graph.Statistics.Value = new UserStatistics
{
+ IsRanked = true,
GlobalRank = 12000,
PP = 12345,
RankHistory = new APIRankHistory
@@ -129,6 +174,7 @@ namespace osu.Game.Tests.Visual.Online
}
};
});
+ AddAssert("line graph shown", () => this.ChildrenOfType().All(graph => graph.Alpha == 1));
}
}
}
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
index d1d3748c26..5e75bd7bc1 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
@@ -11,6 +11,7 @@ using osu.Framework.Graphics;
using osu.Framework.Input.Handlers.Tablet;
using osu.Framework.Testing;
using osu.Framework.Utils;
+using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections.Input;
@@ -36,12 +37,16 @@ namespace osu.Game.Tests.Visual.Settings
Children = new Drawable[]
{
- settings = new TabletSettings(tabletHandler)
+ new OsuScrollContainer(Direction.Vertical)
{
- RelativeSizeAxes = Axes.None,
- Width = SettingsPanel.PANEL_WIDTH,
- Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre,
+ RelativeSizeAxes = Axes.Both,
+ Child = settings = new TabletSettings(tabletHandler)
+ {
+ RelativeSizeAxes = Axes.None,
+ Width = SettingsPanel.PANEL_WIDTH,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ }
}
};
});
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneHistoryTextBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneHistoryTextBox.cs
new file mode 100644
index 0000000000..8ed5dd43cc
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneHistoryTextBox.cs
@@ -0,0 +1,178 @@
+// 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.Graphics;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ [TestFixture]
+ public class TestSceneHistoryTextBox : OsuManualInputManagerTestScene
+ {
+ private const string temp = "Temp message";
+
+ private int messageCounter;
+
+ private HistoryTextBox box = null!;
+ private OsuSpriteText text = null!;
+
+ [SetUp]
+ public void SetUp()
+ {
+ Schedule(() =>
+ {
+ Children = new Drawable[]
+ {
+ box = new HistoryTextBox(5)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.X,
+ Width = 0.99f,
+ },
+ text = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.X,
+ Width = 0.99f,
+ Y = -box.Height,
+ Font = OsuFont.Default.With(size: 20),
+ }
+ };
+
+ box.OnCommit += (_, _) =>
+ {
+ if (string.IsNullOrEmpty(box.Text))
+ return;
+
+ text.Text = $"{nameof(box.OnCommit)}: {box.Text}";
+ box.Text = string.Empty;
+ box.TakeFocus();
+ text.FadeOutFromOne(1000, Easing.InQuint);
+ };
+
+ messageCounter = 0;
+
+ box.TakeFocus();
+ });
+ }
+
+ [Test]
+ public void TestEmptyHistory()
+ {
+ AddStep("Set text", () => box.Text = temp);
+
+ AddStep("Move down", () => InputManager.Key(Key.Down));
+ AddAssert("Text is unchanged", () => box.Text == temp);
+
+ AddStep("Move up", () => InputManager.Key(Key.Up));
+ AddAssert("Text is unchanged", () => box.Text == temp);
+ }
+
+ [Test]
+ public void TestPartialHistory()
+ {
+ addMessages(3);
+ AddStep("Set text", () => box.Text = temp);
+
+ AddStep("Move down", () => InputManager.Key(Key.Down));
+ AddAssert("Text is unchanged", () => box.Text == temp);
+
+ AddRepeatStep("Move up", () => InputManager.Key(Key.Up), 3);
+ AddAssert("Same as 1st message", () => box.Text == "Message 1");
+
+ AddStep("Move up", () => InputManager.Key(Key.Up));
+ AddAssert("Same as 1st message", () => box.Text == "Message 1");
+
+ AddStep("Move down", () => InputManager.Key(Key.Down));
+ AddAssert("Same as 2nd message", () => box.Text == "Message 2");
+
+ AddRepeatStep("Move down", () => InputManager.Key(Key.Down), 2);
+ AddAssert("Temporary message restored", () => box.Text == temp);
+
+ AddStep("Move down", () => InputManager.Key(Key.Down));
+ AddAssert("Text is unchanged", () => box.Text == temp);
+ }
+
+ [Test]
+ public void TestFullHistory()
+ {
+ addMessages(7);
+ AddStep("Set text", () => box.Text = temp);
+
+ AddStep("Move down", () => InputManager.Key(Key.Down));
+ AddAssert("Text is unchanged", () => box.Text == temp);
+
+ AddRepeatStep("Move up", () => InputManager.Key(Key.Up), 5);
+ AddAssert("Same as 3rd message", () => box.Text == "Message 3");
+
+ AddStep("Move up", () => InputManager.Key(Key.Up));
+ AddAssert("Same as 3rd message", () => box.Text == "Message 3");
+
+ AddRepeatStep("Move down", () => InputManager.Key(Key.Down), 4);
+ AddAssert("Same as 7th message", () => box.Text == "Message 7");
+
+ AddStep("Move down", () => InputManager.Key(Key.Down));
+ AddAssert("Temporary message restored", () => box.Text == temp);
+
+ AddStep("Move down", () => InputManager.Key(Key.Down));
+ AddAssert("Text is unchanged", () => box.Text == temp);
+ }
+
+ [Test]
+ public void TestChangedHistory()
+ {
+ addMessages(2);
+ AddStep("Set text", () => box.Text = temp);
+ AddStep("Move up", () => InputManager.Key(Key.Up));
+
+ AddStep("Change text", () => box.Text = "New message");
+ AddStep("Move down", () => InputManager.Key(Key.Down));
+ AddStep("Move up", () => InputManager.Key(Key.Up));
+ AddAssert("Changes lost", () => box.Text == "Message 2");
+ }
+
+ [Test]
+ public void TestInputOnEdge()
+ {
+ addMessages(2);
+ AddStep("Set text", () => box.Text = temp);
+
+ AddStep("Move down", () => InputManager.Key(Key.Down));
+ AddAssert("Text unchanged", () => box.Text == temp);
+
+ AddRepeatStep("Move up", () => InputManager.Key(Key.Up), 2);
+ AddAssert("Same as 1st message", () => box.Text == "Message 1");
+
+ AddStep("Move up", () => InputManager.Key(Key.Up));
+ AddAssert("Text unchanged", () => box.Text == "Message 1");
+ }
+
+ [Test]
+ public void TestResetIndex()
+ {
+ addMessages(2);
+
+ AddRepeatStep("Move Up", () => InputManager.Key(Key.Up), 2);
+ AddAssert("Same as 1st message", () => box.Text == "Message 1");
+
+ AddStep("Change text", () => box.Text = "New message");
+ AddStep("Move Up", () => InputManager.Key(Key.Up));
+ AddAssert("Same as previous message", () => box.Text == "Message 2");
+ }
+
+ private void addMessages(int count)
+ {
+ AddRepeatStep("Add messages", () =>
+ {
+ box.Text = $"Message {++messageCounter}";
+ InputManager.Key(Key.Enter);
+ }, count);
+ }
+ }
+}
diff --git a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs
index 80af4108c7..053ac8fc17 100644
--- a/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs
+++ b/osu.Game/Beatmaps/Drawables/BundledBeatmapDownloader.cs
@@ -136,7 +136,9 @@ namespace osu.Game.Beatmaps.Drawables
private static readonly string[] always_bundled_beatmaps =
{
// This thing is 40mb, I'm not sure we want it here...
- @"1388906 Raphlesia & BilliumMoto - My Love.osz"
+ @"1388906 Raphlesia & BilliumMoto - My Love.osz",
+ // Winner of Triangles mapping competition: https://osu.ppy.sh/home/news/2022-10-06-results-triangles
+ @"1841885 cYsmix - triangles.osz",
};
private static readonly string[] bundled_osu =
diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index 6991500df5..a4e15f790a 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -147,7 +147,11 @@ namespace osu.Game.Beatmaps.Formats
);
}
- protected string CleanFilename(string path) => path.Trim('"').ToStandardisedPath();
+ protected string CleanFilename(string path) => path
+ // User error which is supported by stable (https://github.com/ppy/osu/issues/21204)
+ .Replace(@"\\", @"\")
+ .Trim('"')
+ .ToStandardisedPath();
public enum Section
{
diff --git a/osu.Game/Graphics/UserInterface/Bar.cs b/osu.Game/Graphics/UserInterface/Bar.cs
index 3c87b166ac..e9a20761b3 100644
--- a/osu.Game/Graphics/UserInterface/Bar.cs
+++ b/osu.Game/Graphics/UserInterface/Bar.cs
@@ -109,15 +109,11 @@ namespace osu.Game.Graphics.UserInterface
}
}
- [Flags]
public enum BarDirection
{
- LeftToRight = 1,
- RightToLeft = 1 << 1,
- TopToBottom = 1 << 2,
- BottomToTop = 1 << 3,
-
- Vertical = TopToBottom | BottomToTop,
- Horizontal = LeftToRight | RightToLeft,
+ LeftToRight,
+ RightToLeft,
+ TopToBottom,
+ BottomToTop
}
}
diff --git a/osu.Game/Graphics/UserInterface/BarGraph.cs b/osu.Game/Graphics/UserInterface/BarGraph.cs
index 2e9fd6734f..3f356c0225 100644
--- a/osu.Game/Graphics/UserInterface/BarGraph.cs
+++ b/osu.Game/Graphics/UserInterface/BarGraph.cs
@@ -5,15 +5,23 @@
using osuTK;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using System.Collections.Generic;
using System.Linq;
-using osu.Framework.Extensions.EnumExtensions;
+using osu.Framework.Graphics.Rendering;
+using osu.Framework.Graphics.Shaders;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Graphics.Primitives;
+using osu.Framework.Utils;
+using System;
namespace osu.Game.Graphics.UserInterface
{
- public class BarGraph : FillFlowContainer
+ public class BarGraph : Drawable
{
+ private const int resize_duration = 250;
+ private const Easing easing = Easing.InOutCubic;
+
///
/// Manually sets the max value, if null is instead used
///
@@ -21,22 +29,21 @@ namespace osu.Game.Graphics.UserInterface
private BarDirection direction = BarDirection.BottomToTop;
- public new BarDirection Direction
+ public BarDirection Direction
{
get => direction;
set
{
- direction = value;
- base.Direction = direction.HasFlagFast(BarDirection.Horizontal) ? FillDirection.Vertical : FillDirection.Horizontal;
+ if (direction == value)
+ return;
- foreach (var bar in Children)
- {
- bar.Size = direction.HasFlagFast(BarDirection.Horizontal) ? new Vector2(1, 1.0f / Children.Count) : new Vector2(1.0f / Children.Count, 1);
- bar.Direction = direction;
- }
+ direction = value;
+ Invalidate(Invalidation.DrawNode);
}
}
+ private readonly BarsInfo bars = new BarsInfo();
+
///
/// A list of floats that defines the length of each
///
@@ -44,37 +51,199 @@ namespace osu.Game.Graphics.UserInterface
{
set
{
- List bars = Children.ToList();
-
- foreach (var bar in value.Select((length, index) => new { Value = length, Bar = bars.Count > index ? bars[index] : null }))
+ if (!value.Any())
{
- float length = MaxValue ?? value.Max();
- if (length != 0)
- length = bar.Value / length;
-
- float size = value.Count();
- if (size != 0)
- size = 1.0f / size;
-
- if (bar.Bar != null)
- {
- bar.Bar.Length = length;
- bar.Bar.Size = direction.HasFlagFast(BarDirection.Horizontal) ? new Vector2(1, size) : new Vector2(size, 1);
- }
- else
- {
- Add(new Bar
- {
- RelativeSizeAxes = Axes.Both,
- Size = direction.HasFlagFast(BarDirection.Horizontal) ? new Vector2(1, size) : new Vector2(size, 1),
- Length = length,
- Direction = Direction,
- });
- }
+ bars.Clear();
+ Invalidate(Invalidation.DrawNode);
+ return;
}
- //I'm using ToList() here because Where() returns an Enumerable which can change it's elements afterwards
- RemoveRange(Children.Where((_, index) => index >= value.Count()).ToList(), true);
+ float maxLength = MaxValue ?? value.Max();
+
+ bars.SetLengths(value.Select(v => maxLength == 0 ? 0 : Math.Max(0f, v / maxLength)).ToArray());
+
+ animationStartTime = Clock.CurrentTime;
+ animationComplete = false;
+ }
+ }
+
+ private double animationStartTime;
+ private bool animationComplete;
+
+ private IShader shader = null!;
+ private Texture texture = null!;
+
+ [BackgroundDependencyLoader]
+ private void load(IRenderer renderer, ShaderManager shaders)
+ {
+ texture = renderer.WhitePixel;
+ shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!bars.Any)
+ return;
+
+ double currentTime = Clock.CurrentTime;
+
+ if (currentTime < animationStartTime + resize_duration)
+ {
+ bars.Animate(animationStartTime, currentTime);
+ Invalidate(Invalidation.DrawNode);
+ }
+ else if (!animationComplete)
+ {
+ bars.FinishAnimation();
+ Invalidate(Invalidation.DrawNode);
+
+ animationComplete = true;
+ }
+ }
+
+ protected override DrawNode CreateDrawNode() => new BarGraphDrawNode(this);
+
+ private class BarGraphDrawNode : DrawNode
+ {
+ public new BarGraph Source => (BarGraph)base.Source;
+
+ public BarGraphDrawNode(BarGraph source)
+ : base(source)
+ {
+ }
+
+ private IShader shader = null!;
+ private Texture texture = null!;
+ private Vector2 drawSize;
+ private BarDirection direction;
+ private float barBreadth;
+
+ private readonly List lengths = new List();
+
+ public override void ApplyState()
+ {
+ base.ApplyState();
+
+ shader = Source.shader;
+ texture = Source.texture;
+ drawSize = Source.DrawSize;
+ direction = Source.direction;
+ barBreadth = Source.bars.Breadth;
+
+ lengths.Clear();
+ lengths.AddRange(Source.bars.InstantaneousLengths);
+ }
+
+ public override void Draw(IRenderer renderer)
+ {
+ base.Draw(renderer);
+
+ shader.Bind();
+
+ for (int i = 0; i < lengths.Count; i++)
+ {
+ float barHeight = drawSize.Y * ((direction == BarDirection.TopToBottom || direction == BarDirection.BottomToTop) ? lengths[i] : barBreadth);
+ float barWidth = drawSize.X * ((direction == BarDirection.LeftToRight || direction == BarDirection.RightToLeft) ? lengths[i] : barBreadth);
+
+ Vector2 topLeft;
+
+ switch (direction)
+ {
+ default:
+ case BarDirection.LeftToRight:
+ topLeft = new Vector2(0, i * barHeight);
+ break;
+
+ case BarDirection.RightToLeft:
+ topLeft = new Vector2(drawSize.X - barWidth, i * barHeight);
+ break;
+
+ case BarDirection.TopToBottom:
+ topLeft = new Vector2(i * barWidth, 0);
+ break;
+
+ case BarDirection.BottomToTop:
+ topLeft = new Vector2(i * barWidth, drawSize.Y - barHeight);
+ break;
+ }
+
+ renderer.DrawQuad(
+ texture,
+ new Quad(
+ Vector2Extensions.Transform(topLeft, DrawInfo.Matrix),
+ Vector2Extensions.Transform(topLeft + new Vector2(barWidth, 0), DrawInfo.Matrix),
+ Vector2Extensions.Transform(topLeft + new Vector2(0, barHeight), DrawInfo.Matrix),
+ Vector2Extensions.Transform(topLeft + new Vector2(barWidth, barHeight), DrawInfo.Matrix)
+ ),
+ DrawColourInfo.Colour);
+ }
+
+ shader.Unbind();
+ }
+ }
+
+ private class BarsInfo
+ {
+ public bool Any => Count > 0;
+
+ public int Count { get; private set; }
+
+ public float Breadth { get; private set; }
+
+ public List InstantaneousLengths { get; } = new List();
+
+ private readonly List initialLengths = new List();
+ private readonly List finalLengths = new List();
+
+ public void Clear() => SetLengths(Array.Empty());
+
+ public void SetLengths(float[] newLengths)
+ {
+ int newCount = newLengths.Length;
+
+ for (int i = 0; i < newCount; i++)
+ {
+ // If we have an old bar at this index - change it's length
+ if (i < Count)
+ {
+ initialLengths[i] = finalLengths[i];
+ finalLengths[i] = newLengths[i];
+
+ continue;
+ }
+
+ // If exceeded old bars count - add new one
+ initialLengths.Add(0);
+ finalLengths.Add(newLengths[i]);
+ InstantaneousLengths.Add(0);
+ }
+
+ // Remove excessive bars
+ if (Count > newCount)
+ {
+ int barsToRemove = Count - newCount;
+
+ initialLengths.RemoveRange(newCount, barsToRemove);
+ finalLengths.RemoveRange(newCount, barsToRemove);
+ InstantaneousLengths.RemoveRange(newCount, barsToRemove);
+ }
+
+ Count = newCount;
+ Breadth = Count == 0 ? 0 : (1f / Count);
+ }
+
+ public void Animate(double animationStartTime, double currentTime)
+ {
+ for (int i = 0; i < Count; i++)
+ InstantaneousLengths[i] = Interpolation.ValueAt(currentTime, initialLengths[i], finalLengths[i], animationStartTime, animationStartTime + resize_duration, easing);
+ }
+
+ public void FinishAnimation()
+ {
+ for (int i = 0; i < Count; i++)
+ InstantaneousLengths[i] = finalLengths[i];
}
}
}
diff --git a/osu.Game/Graphics/UserInterface/HistoryTextBox.cs b/osu.Game/Graphics/UserInterface/HistoryTextBox.cs
new file mode 100644
index 0000000000..0958e1832e
--- /dev/null
+++ b/osu.Game/Graphics/UserInterface/HistoryTextBox.cs
@@ -0,0 +1,90 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Input.Events;
+using osu.Game.Utils;
+using osuTK.Input;
+
+namespace osu.Game.Graphics.UserInterface
+{
+ ///
+ /// A which additionally retains a history of text committed, up to a limit
+ /// (100 by default, specified in constructor).
+ /// The history of committed text can be navigated using up/down arrows.
+ /// This resembles the operation of command-line terminals.
+ ///
+ public class HistoryTextBox : FocusedTextBox
+ {
+ private readonly LimitedCapacityQueue messageHistory;
+
+ public int HistoryCount => messageHistory.Count;
+
+ private int selectedIndex;
+
+ private string originalMessage = string.Empty;
+
+ ///
+ /// Creates a new .
+ ///
+ ///
+ /// The maximum number of committed lines to keep in history.
+ /// When exceeded, the oldest lines in history will be dropped to make space for new ones.
+ ///
+ public HistoryTextBox(int capacity = 100)
+ {
+ messageHistory = new LimitedCapacityQueue(capacity);
+
+ Current.ValueChanged += text =>
+ {
+ if (selectedIndex != HistoryCount && text.NewValue != messageHistory[selectedIndex])
+ {
+ selectedIndex = HistoryCount;
+ }
+ };
+ }
+
+ protected override bool OnKeyDown(KeyDownEvent e)
+ {
+ switch (e.Key)
+ {
+ case Key.Up:
+ if (selectedIndex == 0)
+ return true;
+
+ if (selectedIndex == HistoryCount)
+ originalMessage = Text;
+
+ Text = messageHistory[--selectedIndex];
+
+ return true;
+
+ case Key.Down:
+ if (selectedIndex == HistoryCount)
+ return true;
+
+ if (selectedIndex == HistoryCount - 1)
+ {
+ selectedIndex = HistoryCount;
+ Text = originalMessage;
+ return true;
+ }
+
+ Text = messageHistory[++selectedIndex];
+
+ return true;
+ }
+
+ return base.OnKeyDown(e);
+ }
+
+ protected override void Commit()
+ {
+ if (!string.IsNullOrEmpty(Text))
+ messageHistory.Enqueue(Text);
+
+ selectedIndex = HistoryCount;
+
+ base.Commit();
+ }
+ }
+}
diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs
index 81db3f0d53..03728b427f 100644
--- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs
+++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs
@@ -120,17 +120,20 @@ namespace osu.Game.Online.Chat
AddInternal(drawableChannel);
}
- public class ChatTextBox : FocusedTextBox
+ public class ChatTextBox : HistoryTextBox
{
protected override bool OnKeyDown(KeyDownEvent e)
{
// Chat text boxes are generally used in places where they retain focus, but shouldn't block interaction with other
// elements on the same screen.
- switch (e.Key)
+ if (!HoldFocus)
{
- case Key.Up:
- case Key.Down:
- return false;
+ switch (e.Key)
+ {
+ case Key.Up:
+ case Key.Down:
+ return false;
+ }
}
return base.OnKeyDown(e);
diff --git a/osu.Game/Overlays/Chat/ChatTextBox.cs b/osu.Game/Overlays/Chat/ChatTextBox.cs
index 887eb96c15..f0bdbce08d 100644
--- a/osu.Game/Overlays/Chat/ChatTextBox.cs
+++ b/osu.Game/Overlays/Chat/ChatTextBox.cs
@@ -7,7 +7,7 @@ using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Chat
{
- public class ChatTextBox : FocusedTextBox
+ public class ChatTextBox : HistoryTextBox
{
public readonly BindableBool ShowSearch = new BindableBool();
diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs
index 7cb9efa1b9..9b53d62272 100644
--- a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs
@@ -59,7 +59,11 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
// the dropdown. BASS does not give us a simple mechanism to select
// specific audio devices in such a case anyways. Such
// functionality would require involved OS-specific code.
- dropdown.Items = deviceItems.Distinct().ToList();
+ dropdown.Items = deviceItems
+ // Dropdown doesn't like null items. Somehow we are seeing some arrive here (see https://github.com/ppy/osu/issues/21271)
+ .Where(i => i != null)
+ .Distinct()
+ .ToList();
}
protected override void Dispose(bool isDisposing)
diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
index f7c372a037..b6efa00cdb 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -45,9 +45,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input
private GameHost host { get; set; }
///
- /// Based on ultrawide monitor configurations.
+ /// Based on ultrawide monitor configurations, plus a bit of lenience for users which are intentionally aiming for higher horizontal velocity.
///
- private const float largest_feasible_aspect_ratio = 21f / 9;
+ private const float largest_feasible_aspect_ratio = 23f / 9;
private readonly BindableNumber aspectRatio = new BindableFloat(1)
{
diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs
index 1b36ae176d..a9ced62c95 100644
--- a/osu.Game/Scoring/ScoreInfo.cs
+++ b/osu.Game/Scoring/ScoreInfo.cs
@@ -296,6 +296,13 @@ namespace osu.Game.Scoring
break;
}
+ case HitResult.LargeBonus:
+ case HitResult.SmallBonus:
+ if (MaximumStatistics.TryGetValue(r.result, out int count) && count > 0)
+ yield return new HitResultDisplayStatistic(r.result, value, null, r.displayName);
+
+ break;
+
case HitResult.SmallTickMiss:
case HitResult.LargeTickMiss:
break;
diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs
index 5d5019567a..18ea0f69a2 100644
--- a/osu.Game/Screens/Select/SongSelect.cs
+++ b/osu.Game/Screens/Select/SongSelect.cs
@@ -304,6 +304,16 @@ namespace osu.Game.Screens.Select
modSelectOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(ModSelect);
}
+ protected override bool OnScroll(ScrollEvent e)
+ {
+ // Match stable behaviour of only alt-scroll adjusting volume.
+ // Supporting scroll adjust without a modifier key just feels bad, since there are so many scrollable elements on the screen.
+ if (!e.CurrentState.Keyboard.AltPressed)
+ return true;
+
+ return base.OnScroll(e);
+ }
+
///
/// Creates the buttons to be displayed in the footer.
///
diff --git a/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs b/osu.Game/Utils/LimitedCapacityQueue.cs
similarity index 98%
rename from osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs
rename to osu.Game/Utils/LimitedCapacityQueue.cs
index d20eb5e885..86a106a678 100644
--- a/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs
+++ b/osu.Game/Utils/LimitedCapacityQueue.cs
@@ -7,7 +7,7 @@ using System;
using System.Collections;
using System.Collections.Generic;
-namespace osu.Game.Rulesets.Difficulty.Utils
+namespace osu.Game.Utils
{
///
/// An indexed queue with limited capacity.