diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
index e9838de63d..1390675a1a 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
@@ -7,11 +7,13 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
using osuTK;
@@ -23,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
///
/// A visualisation of a single in a .
///
- public class PathControlPointPiece : BlueprintPiece
+ public class PathControlPointPiece : BlueprintPiece, IHasTooltip
{
public Action RequestSelection;
@@ -195,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
markerRing.Alpha = IsSelected.Value ? 1 : 0;
- Color4 colour = ControlPoint.Type.Value != null ? colours.Red : colours.Yellow;
+ Color4 colour = getColourFromNodeType();
if (IsHovered || IsSelected.Value)
colour = colour.Lighten(1);
@@ -203,5 +205,28 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
marker.Colour = colour;
marker.Scale = new Vector2(slider.Scale);
}
+
+ private Color4 getColourFromNodeType()
+ {
+ if (!(ControlPoint.Type.Value is PathType pathType))
+ return colours.Yellow;
+
+ switch (pathType)
+ {
+ case PathType.Catmull:
+ return colours.Seafoam;
+
+ case PathType.Bezier:
+ return colours.Pink;
+
+ case PathType.PerfectCurve:
+ return colours.PurpleDark;
+
+ default:
+ return colours.Red;
+ }
+ }
+
+ public string TooltipText => ControlPoint.Type.Value.ToString() ?? string.Empty;
}
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs
new file mode 100644
index 0000000000..390198be04
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClock.cs
@@ -0,0 +1,81 @@
+// 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.Graphics.Containers;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.Edit.Components;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.Editing
+{
+ [TestFixture]
+ public class TestSceneEditorClock : EditorClockTestScene
+ {
+ public TestSceneEditorClock()
+ {
+ Add(new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new TimeInfoContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(200, 100)
+ },
+ new PlaybackControl
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(200, 100)
+ }
+ }
+ });
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
+ // ensure that music controller does not change this beatmap due to it
+ // completing naturally as part of the test.
+ Beatmap.Disabled = true;
+ }
+
+ [Test]
+ public void TestStopAtTrackEnd()
+ {
+ AddStep("reset clock", () => Clock.Seek(0));
+
+ AddStep("start clock", Clock.Start);
+ AddAssert("clock running", () => Clock.IsRunning);
+
+ AddStep("seek near end", () => Clock.Seek(Clock.TrackLength - 250));
+ AddUntilStep("clock stops", () => !Clock.IsRunning);
+
+ AddAssert("clock stopped at end", () => Clock.CurrentTime == Clock.TrackLength);
+
+ AddStep("start clock again", Clock.Start);
+ AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500);
+ }
+
+ [Test]
+ public void TestWrapWhenStoppedAtTrackEnd()
+ {
+ AddStep("reset clock", () => Clock.Seek(0));
+
+ AddStep("stop clock", Clock.Stop);
+ AddAssert("clock stopped", () => !Clock.IsRunning);
+
+ AddStep("seek exactly to end", () => Clock.Seek(Clock.TrackLength));
+ AddAssert("clock stopped at end", () => Clock.CurrentTime == Clock.TrackLength);
+
+ AddStep("start clock again", Clock.Start);
+ AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs
index d6db171cf0..1da6433707 100644
--- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs
+++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs
@@ -110,8 +110,6 @@ namespace osu.Game.Tests.Visual.Editing
[Resolved]
private EditorClock editorClock { get; set; }
- private bool started;
-
public StartStopButton()
{
BackgroundColour = Color4.SlateGray;
@@ -123,18 +121,17 @@ namespace osu.Game.Tests.Visual.Editing
private void onClick()
{
- if (started)
- {
+ if (editorClock.IsRunning)
editorClock.Stop();
- Text = "Start";
- }
else
- {
editorClock.Start();
- Text = "Stop";
- }
+ }
- started = !started;
+ protected override void Update()
+ {
+ base.Update();
+
+ Text = editorClock.IsRunning ? "Stop" : "Start";
}
}
}
diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
index 0bc5605051..73337ab6f5 100644
--- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Beatmaps.ControlPoints
///
public readonly BindableDouble SpeedMultiplierBindable = new BindableDouble(1)
{
- Precision = 0.1,
+ Precision = 0.01,
Default = 1,
MinValue = 0.1,
MaxValue = 10
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
index 666026e05e..0697dbb392 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
@@ -177,7 +177,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (!track.IsLoaded)
return;
- editorClock.Seek(Current / Content.DrawWidth * track.Length);
+ double target = Current / Content.DrawWidth * track.Length;
+ editorClock.Seek(Math.Min(track.Length, target));
}
private void scrollToTrackTime()
diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs
index ec0f5d7154..d0197ce1ec 100644
--- a/osu.Game/Screens/Edit/EditorClock.cs
+++ b/osu.Game/Screens/Edit/EditorClock.cs
@@ -31,6 +31,8 @@ namespace osu.Game.Screens.Edit
private readonly DecoupleableInterpolatingFramedClock underlyingClock;
+ private bool playbackFinished;
+
public IBindable SeekingOrStopped => seekingOrStopped;
private readonly Bindable seekingOrStopped = new Bindable(true);
@@ -170,6 +172,10 @@ namespace osu.Game.Screens.Edit
public void Start()
{
ClearTransforms();
+
+ if (playbackFinished)
+ underlyingClock.Seek(0);
+
underlyingClock.Start();
}
@@ -216,7 +222,21 @@ namespace osu.Game.Screens.Edit
public bool IsRunning => underlyingClock.IsRunning;
- public void ProcessFrame() => underlyingClock.ProcessFrame();
+ public void ProcessFrame()
+ {
+ underlyingClock.ProcessFrame();
+
+ playbackFinished = CurrentTime >= TrackLength;
+
+ if (playbackFinished)
+ {
+ if (IsRunning)
+ underlyingClock.Stop();
+
+ if (CurrentTime > TrackLength)
+ underlyingClock.Seek(TrackLength);
+ }
+ }
public double ElapsedFrameTime => underlyingClock.ElapsedFrameTime;
diff --git a/osu.Game/Screens/Edit/Timing/DifficultySection.cs b/osu.Game/Screens/Edit/Timing/DifficultySection.cs
index b87b8961f8..9d80ca0b14 100644
--- a/osu.Game/Screens/Edit/Timing/DifficultySection.cs
+++ b/osu.Game/Screens/Edit/Timing/DifficultySection.cs
@@ -18,7 +18,8 @@ namespace osu.Game.Screens.Edit.Timing
{
multiplierSlider = new SliderWithTextBoxInput("Speed Multiplier")
{
- Current = new DifficultyControlPoint().SpeedMultiplierBindable
+ Current = new DifficultyControlPoint().SpeedMultiplierBindable,
+ KeyboardStep = 0.1f
}
});
}
diff --git a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs
index f2f9f76143..10a5771520 100644
--- a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs
+++ b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs
@@ -69,6 +69,15 @@ namespace osu.Game.Screens.Edit.Timing
}, true);
}
+ ///
+ /// A custom step value for each key press which actuates a change on this control.
+ ///
+ public float KeyboardStep
+ {
+ get => slider.KeyboardStep;
+ set => slider.KeyboardStep = value;
+ }
+
public Bindable Current
{
get => slider.Current;
diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs
index f04106dd1c..894a068b7f 100644
--- a/osu.Game/Skinning/SkinManager.cs
+++ b/osu.Game/Skinning/SkinManager.cs
@@ -86,7 +86,7 @@ namespace osu.Game.Skinning
public void SelectRandomSkin()
{
// choose from only user skins, removing the current selection to ensure a new one is chosen.
- var randomChoices = GetAllUsableSkins().Where(s => s.ID > 0 && s.ID != CurrentSkinInfo.Value.ID).ToArray();
+ var randomChoices = GetAllUsableSkins().Where(s => s.ID != CurrentSkinInfo.Value.ID).ToArray();
if (randomChoices.Length == 0)
{