diff --git a/osu.Android.props b/osu.Android.props
index 301c615ce4..3cd4dc48bf 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -54,6 +54,6 @@
-
+
diff --git a/osu.Desktop/Properties/launchSettings.json b/osu.Desktop/Properties/launchSettings.json
new file mode 100644
index 0000000000..5e768ec9fa
--- /dev/null
+++ b/osu.Desktop/Properties/launchSettings.json
@@ -0,0 +1,11 @@
+{
+ "profiles": {
+ "osu! Desktop": {
+ "commandName": "Project"
+ },
+ "osu! Tournament": {
+ "commandName": "Project",
+ "commandLineArgs": "--tournament"
+ }
+ }
+}
\ No newline at end of file
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index 01e4ada2f1..60cada3ae7 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -24,11 +24,11 @@
-
+
-
+
diff --git a/osu.Game.Rulesets.Catch/MathUtils/FastRandom.cs b/osu.Game.Rulesets.Catch/MathUtils/FastRandom.cs
index c721ff862a..46e427e1b7 100644
--- a/osu.Game.Rulesets.Catch/MathUtils/FastRandom.cs
+++ b/osu.Game.Rulesets.Catch/MathUtils/FastRandom.cs
@@ -12,14 +12,14 @@ namespace osu.Game.Rulesets.Catch.MathUtils
{
private const double int_to_real = 1.0 / (int.MaxValue + 1.0);
private const uint int_mask = 0x7FFFFFFF;
- private const uint y = 842502087;
- private const uint z = 3579807591;
- private const uint w = 273326509;
- private uint _x, _y = y, _z = z, _w = w;
+ private const uint y_initial = 842502087;
+ private const uint z_initial = 3579807591;
+ private const uint w_initial = 273326509;
+ private uint x, y = y_initial, z = z_initial, w = w_initial;
public FastRandom(int seed)
{
- _x = (uint)seed;
+ x = (uint)seed;
}
public FastRandom()
@@ -33,11 +33,11 @@ namespace osu.Game.Rulesets.Catch.MathUtils
/// The random value.
public uint NextUInt()
{
- uint t = _x ^ (_x << 11);
- _x = _y;
- _y = _z;
- _z = _w;
- return _w = _w ^ (_w >> 19) ^ t ^ (t >> 8);
+ uint t = x ^ (x << 11);
+ x = y;
+ y = z;
+ z = w;
+ return w = w ^ (w >> 19) ^ t ^ (t >> 8);
}
///
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index 33780427b6..d5d99640af 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -116,7 +116,23 @@ namespace osu.Game.Rulesets.Catch.Objects
public double Duration => EndTime - StartTime;
- public SliderPath Path { get; set; }
+ private readonly SliderPath path = new SliderPath();
+
+ public SliderPath Path
+ {
+ get => path;
+ set
+ {
+ path.ControlPoints.Clear();
+ path.ExpectedDistance.Value = null;
+
+ if (value != null)
+ {
+ path.ControlPoints.AddRange(value.ControlPoints);
+ path.ExpectedDistance.Value = value.ExpectedDistance.Value;
+ }
+ }
+ }
public double Distance => Path.Distance;
diff --git a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs
index e3c6c93d01..025fa9c56e 100644
--- a/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatcherSprite.cs
@@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.UI
[BackgroundDependencyLoader]
private void load()
{
- InternalChild = new SkinnableSprite("Gameplay/catch/fruit-catcher-idle")
+ InternalChild = new SkinnableSprite("Gameplay/catch/fruit-catcher-idle", confineMode: ConfineMode.ScaleDownToFit)
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.TopCentre,
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
index 693faee3b7..85a41137d4 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
- [TestCase(6.931145117263422, "diffcalc-test")]
+ [TestCase(6.9311451172608853d, "diffcalc-test")]
[TestCase(1.0736587013228804d, "zero-length-sliders")]
public void Test(double expected, string name)
=> base.Test(expected, name);
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs
index dde2aa53e0..013920684c 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs
@@ -196,7 +196,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
AddStep($"move mouse to control point {index}", () =>
{
- Vector2 position = slider.Position + slider.Path.ControlPoints[index];
+ Vector2 position = slider.Position + slider.Path.ControlPoints[index].Position.Value;
InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
});
}
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 0ccf020300..c2aefac587 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
@@ -11,6 +11,7 @@ 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.Osu.Objects;
using osuTK;
using osuTK.Graphics;
@@ -20,11 +21,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
public class PathControlPointPiece : BlueprintPiece
{
- public Action RequestSelection;
- public Action ControlPointsChanged;
+ public Action RequestSelection;
public readonly BindableBool IsSelected = new BindableBool();
- public readonly int Index;
+
+ public readonly PathControlPoint ControlPoint;
private readonly Slider slider;
private readonly Path path;
@@ -37,10 +38,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
[Resolved]
private OsuColour colours { get; set; }
- public PathControlPointPiece(Slider slider, int index)
+ private IBindable sliderPosition;
+ private IBindable pathVersion;
+
+ public PathControlPointPiece(Slider slider, PathControlPoint controlPoint)
{
this.slider = slider;
- Index = index;
+
+ ControlPoint = controlPoint;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
@@ -86,48 +91,41 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
};
}
- protected override void Update()
+ protected override void LoadComplete()
{
- base.Update();
+ base.LoadComplete();
- Position = slider.StackedPosition + slider.Path.ControlPoints[Index];
+ sliderPosition = slider.PositionBindable.GetBoundCopy();
+ sliderPosition.BindValueChanged(_ => updateDisplay());
+ pathVersion = slider.Path.Version.GetBoundCopy();
+ pathVersion.BindValueChanged(_ => updateDisplay());
+
+ IsSelected.BindValueChanged(_ => updateMarkerDisplay());
+
+ updateDisplay();
+ }
+
+ private void updateDisplay()
+ {
updateMarkerDisplay();
updateConnectingPath();
}
- ///
- /// Updates the state of the circular control point marker.
- ///
- private void updateMarkerDisplay()
- {
- markerRing.Alpha = IsSelected.Value ? 1 : 0;
-
- Color4 colour = isSegmentSeparator ? colours.Red : colours.Yellow;
- if (IsHovered || IsSelected.Value)
- colour = Color4.White;
- marker.Colour = colour;
- }
-
- ///
- /// Updates the path connecting this control point to the previous one.
- ///
- private void updateConnectingPath()
- {
- path.ClearVertices();
-
- if (Index != slider.Path.ControlPoints.Length - 1)
- {
- path.AddVertex(Vector2.Zero);
- path.AddVertex(slider.Path.ControlPoints[Index + 1] - slider.Path.ControlPoints[Index]);
- }
-
- path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
- }
-
// The connecting path is excluded from positional input
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => marker.ReceivePositionalInputAt(screenSpacePos);
+ protected override bool OnHover(HoverEvent e)
+ {
+ updateMarkerDisplay();
+ return false;
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ updateMarkerDisplay();
+ }
+
protected override bool OnMouseDown(MouseDownEvent e)
{
if (RequestSelection == null)
@@ -136,12 +134,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
switch (e.Button)
{
case MouseButton.Left:
- RequestSelection.Invoke(Index, e);
+ RequestSelection.Invoke(this, e);
return true;
case MouseButton.Right:
if (!IsSelected.Value)
- RequestSelection.Invoke(Index, e);
+ RequestSelection.Invoke(this, e);
return false; // Allow context menu to show
}
@@ -156,9 +154,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
protected override bool OnDrag(DragEvent e)
{
- var newControlPoints = slider.Path.ControlPoints.ToArray();
-
- if (Index == 0)
+ if (ControlPoint == slider.Path.ControlPoints[0])
{
// Special handling for the head control point - the position of the slider changes which means the snapped position and time have to be taken into account
(Vector2 snappedPosition, double snappedTime) = snapProvider?.GetSnappedPosition(e.MousePosition, slider.StartTime) ?? (e.MousePosition, slider.StartTime);
@@ -168,29 +164,51 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
slider.StartTime = snappedTime;
// Since control points are relative to the position of the slider, they all need to be offset backwards by the delta
- for (int i = 1; i < newControlPoints.Length; i++)
- newControlPoints[i] -= movementDelta;
+ for (int i = 1; i < slider.Path.ControlPoints.Count; i++)
+ slider.Path.ControlPoints[i].Position.Value -= movementDelta;
}
else
- newControlPoints[Index] += e.Delta;
-
- if (isSegmentSeparatorWithNext)
- newControlPoints[Index + 1] = newControlPoints[Index];
-
- if (isSegmentSeparatorWithPrevious)
- newControlPoints[Index - 1] = newControlPoints[Index];
-
- ControlPointsChanged?.Invoke(newControlPoints);
+ ControlPoint.Position.Value += e.Delta;
return true;
}
protected override bool OnDragEnd(DragEndEvent e) => true;
- private bool isSegmentSeparator => isSegmentSeparatorWithNext || isSegmentSeparatorWithPrevious;
+ ///
+ /// Updates the state of the circular control point marker.
+ ///
+ private void updateMarkerDisplay()
+ {
+ Position = slider.StackedPosition + ControlPoint.Position.Value;
- private bool isSegmentSeparatorWithNext => Index < slider.Path.ControlPoints.Length - 1 && slider.Path.ControlPoints[Index + 1] == slider.Path.ControlPoints[Index];
+ markerRing.Alpha = IsSelected.Value ? 1 : 0;
- private bool isSegmentSeparatorWithPrevious => Index > 0 && slider.Path.ControlPoints[Index - 1] == slider.Path.ControlPoints[Index];
+ Color4 colour = ControlPoint.Type.Value != null ? colours.Red : colours.Yellow;
+ if (IsHovered || IsSelected.Value)
+ colour = Color4.White;
+ marker.Colour = colour;
+ }
+
+ ///
+ /// Updates the path connecting this control point to the previous one.
+ ///
+ private void updateConnectingPath()
+ {
+ path.ClearVertices();
+
+ int index = slider.Path.ControlPoints.IndexOf(ControlPoint);
+
+ if (index == -1)
+ return;
+
+ if (++index != slider.Path.ControlPoints.Count)
+ {
+ path.AddVertex(Vector2.Zero);
+ path.AddVertex(slider.Path.ControlPoints[index].Position.Value - ControlPoint.Position.Value);
+ }
+
+ path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
index cdca48490e..22155ab7af 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using Humanizer;
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
@@ -14,6 +15,8 @@ using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose;
using osuTK;
@@ -23,10 +26,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler, IHasContextMenu
{
- public Action ControlPointsChanged;
-
internal readonly Container Pieces;
+
private readonly Slider slider;
+
private readonly bool allowSelection;
private InputManager inputManager;
@@ -34,6 +37,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
[Resolved(CanBeNull = true)]
private IPlacementHandler placementHandler { get; set; }
+ private IBindableList controlPoints;
+
public PathControlPointVisualiser(Slider slider, bool allowSelection)
{
this.slider = slider;
@@ -49,33 +54,40 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
base.LoadComplete();
inputManager = GetContainingInputManager();
+
+ controlPoints = slider.Path.ControlPoints.GetBoundCopy();
+ controlPoints.ItemsAdded += addControlPoints;
+ controlPoints.ItemsRemoved += removeControlPoints;
+
+ addControlPoints(controlPoints);
}
- protected override void Update()
+ private void addControlPoints(IEnumerable controlPoints)
{
- base.Update();
-
- while (slider.Path.ControlPoints.Length > Pieces.Count)
+ foreach (var point in controlPoints)
{
- var piece = new PathControlPointPiece(slider, Pieces.Count)
- {
- ControlPointsChanged = c => ControlPointsChanged?.Invoke(c),
- };
+ var piece = new PathControlPointPiece(slider, point);
if (allowSelection)
piece.RequestSelection = selectPiece;
Pieces.Add(piece);
}
+ }
- while (slider.Path.ControlPoints.Length < Pieces.Count)
- Pieces.Remove(Pieces[Pieces.Count - 1]);
+ private void removeControlPoints(IEnumerable controlPoints)
+ {
+ foreach (var point in controlPoints)
+ Pieces.RemoveAll(p => p.ControlPoint == point);
}
protected override bool OnClick(ClickEvent e)
{
foreach (var piece in Pieces)
+ {
piece.IsSelected.Value = false;
+ }
+
return false;
}
@@ -92,51 +104,53 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public bool OnReleased(PlatformAction action) => action.ActionMethod == PlatformActionMethod.Delete;
- private void selectPiece(int index, MouseButtonEvent e)
+ private void selectPiece(PathControlPointPiece piece, MouseButtonEvent e)
{
if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed)
- Pieces[index].IsSelected.Toggle();
+ piece.IsSelected.Toggle();
else
{
- foreach (var piece in Pieces)
- piece.IsSelected.Value = piece.Index == index;
+ foreach (var p in Pieces)
+ p.IsSelected.Value = p == piece;
}
}
private bool deleteSelected()
{
- var newControlPoints = new List();
-
- foreach (var piece in Pieces)
- {
- if (!piece.IsSelected.Value)
- newControlPoints.Add(slider.Path.ControlPoints[piece.Index]);
- }
+ List toRemove = Pieces.Where(p => p.IsSelected.Value).Select(p => p.ControlPoint).ToList();
// Ensure that there are any points to be deleted
- if (newControlPoints.Count == slider.Path.ControlPoints.Length)
+ if (toRemove.Count == 0)
return false;
- // If there are 0 remaining control points, treat the slider as being deleted
- if (newControlPoints.Count == 0)
+ foreach (var c in toRemove)
+ {
+ // The first control point in the slider must have a type, so take it from the previous "first" one
+ // Todo: Should be handled within SliderPath itself
+ if (c == slider.Path.ControlPoints[0] && slider.Path.ControlPoints.Count > 1 && slider.Path.ControlPoints[1].Type.Value == null)
+ slider.Path.ControlPoints[1].Type.Value = slider.Path.ControlPoints[0].Type.Value;
+
+ slider.Path.ControlPoints.Remove(c);
+ }
+
+ // If there are 0 or 1 remaining control points, the slider is in a degenerate (single point) form and should be deleted
+ if (slider.Path.ControlPoints.Count <= 1)
{
placementHandler?.Delete(slider);
return true;
}
- // Make control points relative
- Vector2 first = newControlPoints[0];
- for (int i = 0; i < newControlPoints.Count; i++)
- newControlPoints[i] = newControlPoints[i] - first;
-
- // The slider's position defines the position of the first control point, and all further control points are relative to that point
+ // The path will have a non-zero offset if the head is removed, but sliders don't support this behaviour since the head is positioned at the slider's position
+ // So the slider needs to be offset by this amount instead, and all control points offset backwards such that the path is re-positioned at (0, 0)
+ Vector2 first = slider.Path.ControlPoints[0].Position.Value;
+ foreach (var c in slider.Path.ControlPoints)
+ c.Position.Value -= first;
slider.Position += first;
// Since pieces are re-used, they will not point to the deleted control points while remaining selected
foreach (var piece in Pieces)
piece.IsSelected.Value = false;
- ControlPointsChanged?.Invoke(newControlPoints.ToArray());
return true;
}
@@ -147,16 +161,63 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (!Pieces.Any(p => p.IsHovered))
return null;
- int selectedPoints = Pieces.Count(p => p.IsSelected.Value);
+ var selectedPieces = Pieces.Where(p => p.IsSelected.Value).ToList();
+ int count = selectedPieces.Count;
- if (selectedPoints == 0)
+ if (count == 0)
return null;
+ List
public class SliderTailCircle : SliderCircle
{
- private readonly IBindable pathBindable = new Bindable();
+ private readonly IBindable pathVersion = new Bindable();
public SliderTailCircle(Slider slider)
{
- pathBindable.BindTo(slider.PathBindable);
- pathBindable.BindValueChanged(_ => Position = slider.EndPosition);
+ pathVersion.BindTo(slider.Path.Version);
+ pathVersion.BindValueChanged(_ => Position = slider.EndPosition);
}
public override Judgement CreateJudgement() => new OsuSliderTailJudgement();
diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyCursor.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyCursor.cs
index 470ba3acae..02152fa51e 100644
--- a/osu.Game.Rulesets.Osu/Skinning/LegacyCursor.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/LegacyCursor.cs
@@ -3,14 +3,16 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Game.Skinning;
+using osu.Game.Rulesets.Osu.UI.Cursor;
using osuTK;
namespace osu.Game.Rulesets.Osu.Skinning
{
- public class LegacyCursor : CompositeDrawable
+ public class LegacyCursor : OsuCursorSprite
{
+ private bool spin;
+
public LegacyCursor()
{
Size = new Vector2(50);
@@ -22,7 +24,9 @@ namespace osu.Game.Rulesets.Osu.Skinning
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
- InternalChildren = new Drawable[]
+ spin = skin.GetConfig(OsuSkinConfiguration.CursorRotate)?.Value ?? true;
+
+ InternalChildren = new[]
{
new NonPlayfieldSprite
{
@@ -30,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
- new NonPlayfieldSprite
+ ExpandTarget = new NonPlayfieldSprite
{
Texture = skin.GetTexture("cursor"),
Anchor = Anchor.Centre,
@@ -38,5 +42,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
}
};
}
+
+ protected override void LoadComplete()
+ {
+ if (spin)
+ ExpandTarget.Spin(10000, RotationDirection.Clockwise);
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
index 98219cafe8..5d99960f10 100644
--- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
@@ -11,5 +11,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
SliderPathRadius,
AllowSliderBallTint,
CursorExpand,
+ CursorRotate
}
}
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs
index 0aa8661fd3..4f3d07f208 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursor.cs
@@ -20,7 +20,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private bool cursorExpand;
- private Container expandTarget;
+ private SkinnableDrawable cursorSprite;
+
+ private Drawable expandTarget => (cursorSprite.Drawable as OsuCursorSprite)?.ExpandTarget ?? cursorSprite;
public OsuCursor()
{
@@ -37,12 +39,12 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
[BackgroundDependencyLoader]
private void load()
{
- InternalChild = expandTarget = new Container
+ InternalChild = new Container
{
RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
- Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.Cursor), _ => new DefaultCursor(), confineMode: ConfineMode.NoScaling)
+ Child = cursorSprite = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.Cursor), _ => new DefaultCursor(), confineMode: ConfineMode.NoScaling)
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
@@ -62,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
public void Contract() => expandTarget.ScaleTo(released_scale, 100, Easing.OutQuad);
- private class DefaultCursor : CompositeDrawable
+ private class DefaultCursor : OsuCursorSprite
{
public DefaultCursor()
{
@@ -71,10 +73,12 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
- InternalChildren = new Drawable[]
+ InternalChildren = new[]
{
- new CircularContainer
+ ExpandTarget = new CircularContainer
{
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = size / 6,
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorSprite.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorSprite.cs
new file mode 100644
index 0000000000..573c408a78
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorSprite.cs
@@ -0,0 +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 osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+
+namespace osu.Game.Rulesets.Osu.UI.Cursor
+{
+ public abstract class OsuCursorSprite : CompositeDrawable
+ {
+ ///
+ /// The an optional piece of the cursor to expand when in a clicked state.
+ /// If null, the whole cursor will be affected by expansion.
+ ///
+ public Drawable ExpandTarget { get; protected set; }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs
index e4c987923c..39b4bf7218 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs
+++ b/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs
@@ -7,7 +7,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
-using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
@@ -44,7 +43,7 @@ namespace osu.Game.Tests.Visual.Editor
RelativeSizeAxes = Axes.Both,
Colour = Color4.SlateGray
},
- new TestDistanceSnapGrid(new HitObject(), grid_position)
+ new TestDistanceSnapGrid()
};
});
@@ -73,7 +72,7 @@ namespace osu.Game.Tests.Visual.Editor
RelativeSizeAxes = Axes.Both,
Colour = Color4.SlateGray
},
- new TestDistanceSnapGrid(new HitObject(), grid_position, new HitObject { StartTime = 100 })
+ new TestDistanceSnapGrid(100)
};
});
}
@@ -82,68 +81,68 @@ namespace osu.Game.Tests.Visual.Editor
{
public new float DistanceSpacing => base.DistanceSpacing;
- public TestDistanceSnapGrid(HitObject hitObject, Vector2 centrePosition, HitObject nextHitObject = null)
- : base(hitObject, nextHitObject, centrePosition)
+ public TestDistanceSnapGrid(double? endTime = null)
+ : base(grid_position, 0, endTime)
{
}
- protected override void CreateContent(Vector2 centrePosition)
+ protected override void CreateContent(Vector2 startPosition)
{
AddInternal(new Circle
{
Origin = Anchor.Centre,
Size = new Vector2(5),
- Position = centrePosition
+ Position = startPosition
});
int beatIndex = 0;
- for (float s = centrePosition.X + DistanceSpacing; s <= DrawWidth && beatIndex < MaxIntervals; s += DistanceSpacing, beatIndex++)
+ for (float s = startPosition.X + DistanceSpacing; s <= DrawWidth && beatIndex < MaxIntervals; s += DistanceSpacing, beatIndex++)
{
AddInternal(new Circle
{
Origin = Anchor.Centre,
Size = new Vector2(5, 10),
- Position = new Vector2(s, centrePosition.Y),
+ Position = new Vector2(s, startPosition.Y),
Colour = GetColourForBeatIndex(beatIndex)
});
}
beatIndex = 0;
- for (float s = centrePosition.X - DistanceSpacing; s >= 0 && beatIndex < MaxIntervals; s -= DistanceSpacing, beatIndex++)
+ for (float s = startPosition.X - DistanceSpacing; s >= 0 && beatIndex < MaxIntervals; s -= DistanceSpacing, beatIndex++)
{
AddInternal(new Circle
{
Origin = Anchor.Centre,
Size = new Vector2(5, 10),
- Position = new Vector2(s, centrePosition.Y),
+ Position = new Vector2(s, startPosition.Y),
Colour = GetColourForBeatIndex(beatIndex)
});
}
beatIndex = 0;
- for (float s = centrePosition.Y + DistanceSpacing; s <= DrawHeight && beatIndex < MaxIntervals; s += DistanceSpacing, beatIndex++)
+ for (float s = startPosition.Y + DistanceSpacing; s <= DrawHeight && beatIndex < MaxIntervals; s += DistanceSpacing, beatIndex++)
{
AddInternal(new Circle
{
Origin = Anchor.Centre,
Size = new Vector2(10, 5),
- Position = new Vector2(centrePosition.X, s),
+ Position = new Vector2(startPosition.X, s),
Colour = GetColourForBeatIndex(beatIndex)
});
}
beatIndex = 0;
- for (float s = centrePosition.Y - DistanceSpacing; s >= 0 && beatIndex < MaxIntervals; s -= DistanceSpacing, beatIndex++)
+ for (float s = startPosition.Y - DistanceSpacing; s >= 0 && beatIndex < MaxIntervals; s -= DistanceSpacing, beatIndex++)
{
AddInternal(new Circle
{
Origin = Anchor.Centre,
Size = new Vector2(10, 5),
- Position = new Vector2(centrePosition.X, s),
+ Position = new Vector2(startPosition.X, s),
Colour = GetColourForBeatIndex(beatIndex)
});
}
diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs
index 6e5b3b93e9..e618256c03 100644
--- a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs
+++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs
@@ -13,6 +13,8 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK;
using osuTK.Graphics;
@@ -25,6 +27,7 @@ namespace osu.Game.Tests.Visual.Editor
public override IReadOnlyList RequiredTypes => new[]
{
typeof(TimelineArea),
+ typeof(TimelineHitObjectDisplay),
typeof(Timeline),
typeof(TimelineButton),
typeof(CentreMarker)
@@ -35,6 +38,8 @@ namespace osu.Game.Tests.Visual.Editor
{
Beatmap.Value = new WaveformTestBeatmap(audio);
+ var editorBeatmap = new EditorBeatmap((Beatmap)Beatmap.Value.Beatmap);
+
Children = new Drawable[]
{
new FillFlowContainer
@@ -50,6 +55,7 @@ namespace osu.Game.Tests.Visual.Editor
},
new TimelineArea
{
+ Child = new TimelineHitObjectDisplay(editorBeatmap),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
index 803cab9325..e04315894e 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs
@@ -285,8 +285,6 @@ namespace osu.Game.Tests.Visual.Gameplay
protected class PausePlayer : TestPlayer
{
- public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer;
-
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
public new HUDOverlay HUDOverlay => base.HUDOverlay;
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs
new file mode 100644
index 0000000000..3513b6c25a
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs
@@ -0,0 +1,51 @@
+// 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.Bindables;
+using osu.Framework.Platform;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Screens.Play;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ [HeadlessTest] // we alter unsafe properties on the game host to test inactive window state.
+ public class TestScenePauseWhenInactive : PlayerTestScene
+ {
+ protected new TestPlayer Player => (TestPlayer)base.Player;
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
+ {
+ var beatmap = (Beatmap)base.CreateBeatmap(ruleset);
+
+ beatmap.HitObjects.RemoveAll(h => h.StartTime < 30000);
+
+ return beatmap;
+ }
+
+ [Resolved]
+ private GameHost host { get; set; }
+
+ public TestScenePauseWhenInactive()
+ : base(new OsuRuleset())
+ {
+ }
+
+ [Test]
+ public void TestDoesntPauseDuringIntro()
+ {
+ AddStep("set inactive", () => ((Bindable)host.IsActive).Value = false);
+
+ AddStep("resume player", () => Player.GameplayClockContainer.Start());
+ AddAssert("ensure not paused", () => !Player.GameplayClockContainer.IsPaused.Value);
+ AddUntilStep("wait for pause", () => Player.GameplayClockContainer.IsPaused.Value);
+ AddAssert("time of pause is after gameplay start time", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= Player.DrawableRuleset.GameplayStartTime);
+ }
+
+ protected override Player CreatePlayer(Ruleset ruleset) => new TestPlayer(true, true, true);
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
index dbea8d28a6..f02361e685 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs
@@ -57,8 +57,8 @@ namespace osu.Game.Tests.Visual.Gameplay
beforeLoadAction?.Invoke();
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
- foreach (var mod in Mods.Value.OfType())
- mod.ApplyToClock(Beatmap.Value.Track);
+ foreach (var mod in Mods.Value.OfType())
+ mod.ApplyToTrack(Beatmap.Value.Track);
InputManager.Child = container = new TestPlayerLoaderContainer(
loader = new TestPlayerLoader(() =>
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs
new file mode 100644
index 0000000000..606395c289
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSliderPath.cs
@@ -0,0 +1,193 @@
+// 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 System.Linq;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Lines;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneSliderPath : OsuTestScene
+ {
+ private readonly SmoothPath drawablePath;
+ private SliderPath path;
+
+ public TestSceneSliderPath()
+ {
+ Child = drawablePath = new SmoothPath
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre
+ };
+ }
+
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ path = new SliderPath();
+ });
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (path != null)
+ {
+ List vertices = new List();
+ path.GetPathToProgress(vertices, 0, 1);
+
+ drawablePath.Vertices = vertices;
+ }
+ }
+
+ [Test]
+ public void TestEmptyPath()
+ {
+ }
+
+ [TestCase(PathType.Linear)]
+ [TestCase(PathType.Bezier)]
+ [TestCase(PathType.Catmull)]
+ [TestCase(PathType.PerfectCurve)]
+ public void TestSingleSegment(PathType type)
+ => AddStep("create path", () => path.ControlPoints.AddRange(createSegment(type, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
+
+ [TestCase(PathType.Linear)]
+ [TestCase(PathType.Bezier)]
+ [TestCase(PathType.Catmull)]
+ [TestCase(PathType.PerfectCurve)]
+ public void TestMultipleSegment(PathType type)
+ {
+ AddStep("create path", () =>
+ {
+ path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero));
+ path.ControlPoints.AddRange(createSegment(type, new Vector2(0, 100), new Vector2(100), Vector2.Zero));
+ });
+ }
+
+ [Test]
+ public void TestAddControlPoint()
+ {
+ AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100))));
+ AddStep("add point", () => path.ControlPoints.Add(new PathControlPoint { Position = { Value = new Vector2(100) } }));
+ }
+
+ [Test]
+ public void TestInsertControlPoint()
+ {
+ AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(100))));
+ AddStep("insert point", () => path.ControlPoints.Insert(1, new PathControlPoint { Position = { Value = new Vector2(0, 100) } }));
+ }
+
+ [Test]
+ public void TestRemoveControlPoint()
+ {
+ AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
+ AddStep("remove second point", () => path.ControlPoints.RemoveAt(1));
+ }
+
+ [Test]
+ public void TestChangePathType()
+ {
+ AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
+ AddStep("change type to bezier", () => path.ControlPoints[0].Type.Value = PathType.Bezier);
+ }
+
+ [Test]
+ public void TestAddSegmentByChangingType()
+ {
+ AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0))));
+ AddStep("change second point type to bezier", () => path.ControlPoints[1].Type.Value = PathType.Bezier);
+ }
+
+ [Test]
+ public void TestRemoveSegmentByChangingType()
+ {
+ AddStep("create path", () =>
+ {
+ path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
+ path.ControlPoints[1].Type.Value = PathType.Bezier;
+ });
+
+ AddStep("change second point type to null", () => path.ControlPoints[1].Type.Value = null);
+ }
+
+ [Test]
+ public void TestRemoveSegmentByRemovingControlPoint()
+ {
+ AddStep("create path", () =>
+ {
+ path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
+ path.ControlPoints[1].Type.Value = PathType.Bezier;
+ });
+
+ AddStep("remove second point", () => path.ControlPoints.RemoveAt(1));
+ }
+
+ [TestCase(2)]
+ [TestCase(4)]
+ public void TestPerfectCurveFallbackScenarios(int points)
+ {
+ AddStep("create path", () =>
+ {
+ switch (points)
+ {
+ case 2:
+ path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100)));
+ break;
+
+ case 4:
+ path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(0, 100), new Vector2(100), new Vector2(100, 0)));
+ break;
+ }
+ });
+ }
+
+ [Test]
+ public void TestLengthenLastSegment()
+ {
+ AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
+ AddStep("lengthen last segment", () => path.ExpectedDistance.Value = 300);
+ }
+
+ [Test]
+ public void TestShortenLastSegment()
+ {
+ AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
+ AddStep("shorten last segment", () => path.ExpectedDistance.Value = 150);
+ }
+
+ [Test]
+ public void TestShortenFirstSegment()
+ {
+ AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
+ AddStep("shorten first segment", () => path.ExpectedDistance.Value = 50);
+ }
+
+ [Test]
+ public void TestShortenToZeroLength()
+ {
+ AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
+ AddStep("shorten to 0 length", () => path.ExpectedDistance.Value = 0);
+ }
+
+ [Test]
+ public void TestShortenToNegativeLength()
+ {
+ AddStep("create path", () => path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(0, 100), new Vector2(100))));
+ AddStep("shorten to -10 length", () => path.ExpectedDistance.Value = -10);
+ }
+
+ private List createSegment(PathType type, params Vector2[] controlPoints)
+ {
+ var points = controlPoints.Select(p => new PathControlPoint { Position = { Value = p } }).ToList();
+ points[0].Type.Value = type;
+ return points;
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs
new file mode 100644
index 0000000000..e3dae9c27e
--- /dev/null
+++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs
@@ -0,0 +1,65 @@
+// 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.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Configuration;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.Settings
+{
+ [TestFixture]
+ public class TestSceneSettingsSource : OsuTestScene
+ {
+ public TestSceneSettingsSource()
+ {
+ Children = new Drawable[]
+ {
+ new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(20),
+ Width = 0.5f,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Padding = new MarginPadding(50),
+ ChildrenEnumerable = new TestTargetClass().CreateSettingsControls()
+ },
+ };
+ }
+
+ private class TestTargetClass
+ {
+ [SettingSource("Sample bool", "Clicking this changes a setting")]
+ public BindableBool TickBindable { get; } = new BindableBool();
+
+ [SettingSource("Sample float", "Change something for a mod")]
+ public BindableFloat SliderBindable { get; } = new BindableFloat
+ {
+ MinValue = 0,
+ MaxValue = 10,
+ Default = 5,
+ Value = 7
+ };
+
+ [SettingSource("Sample enum", "Change something for a mod")]
+ public Bindable EnumBindable { get; } = new Bindable
+ {
+ Default = TestEnum.Value1,
+ Value = TestEnum.Value2
+ };
+
+ [SettingSource("Sample string", "Change something for a mod")]
+ public Bindable StringBindable { get; } = new Bindable();
+ }
+
+ private enum TestEnum
+ {
+ Value1,
+ Value2
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index a4b8d1a24a..5dd02c1ddd 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -95,6 +95,42 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("filter count is 1", () => songSelect.FilterCount == 1);
}
+ [Test]
+ public void TestNoFilterOnSimpleResume()
+ {
+ addRulesetImportStep(0);
+ addRulesetImportStep(0);
+
+ createSongSelect();
+
+ AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
+ AddUntilStep("wait for not current", () => !songSelect.IsCurrentScreen());
+
+ AddStep("return", () => songSelect.MakeCurrent());
+ AddUntilStep("wait for current", () => songSelect.IsCurrentScreen());
+ AddAssert("filter count is 1", () => songSelect.FilterCount == 1);
+ }
+
+ [Test]
+ public void TestFilterOnResumeAfterChange()
+ {
+ addRulesetImportStep(0);
+ addRulesetImportStep(0);
+
+ AddStep("change convert setting", () => config.Set(OsuSetting.ShowConvertedBeatmaps, false));
+
+ createSongSelect();
+
+ AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
+ AddUntilStep("wait for not current", () => !songSelect.IsCurrentScreen());
+
+ AddStep("change convert setting", () => config.Set(OsuSetting.ShowConvertedBeatmaps, true));
+
+ AddStep("return", () => songSelect.MakeCurrent());
+ AddUntilStep("wait for current", () => songSelect.IsCurrentScreen());
+ AddAssert("filter count is 2", () => songSelect.FilterCount == 2);
+ }
+
[Test]
public void TestAudioResuming()
{
diff --git a/osu.Game.Tests/Visual/TestSceneOsuScreenStack.cs b/osu.Game.Tests/Visual/TestSceneOsuScreenStack.cs
index a68fd0ef40..c55988d1bb 100644
--- a/osu.Game.Tests/Visual/TestSceneOsuScreenStack.cs
+++ b/osu.Game.Tests/Visual/TestSceneOsuScreenStack.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual
AddAssert("Parallax is off", () => stack.ParallaxAmount == 0);
}
- private class TestScreen : ScreenWithBeatmapBackground
+ public class TestScreen : ScreenWithBeatmapBackground
{
private readonly string screenText;
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs
new file mode 100644
index 0000000000..fc44c5f595
--- /dev/null
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs
@@ -0,0 +1,107 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Configuration;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays.Mods;
+using osu.Game.Rulesets.Mods;
+
+namespace osu.Game.Tests.Visual.UserInterface
+{
+ public class TestSceneModSettings : OsuTestScene
+ {
+ private TestModSelectOverlay modSelect;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Add(modSelect = new TestModSelectOverlay
+ {
+ RelativeSizeAxes = Axes.X,
+ Origin = Anchor.BottomCentre,
+ Anchor = Anchor.BottomCentre,
+ });
+
+ var testMod = new TestModCustomisable1();
+
+ AddStep("open", modSelect.Show);
+ AddAssert("button disabled", () => !modSelect.CustomiseButton.Enabled.Value);
+ AddUntilStep("wait for button load", () => modSelect.ButtonsLoaded);
+ AddStep("select mod", () => modSelect.SelectMod(testMod));
+ AddAssert("button enabled", () => modSelect.CustomiseButton.Enabled.Value);
+ AddStep("open Customisation", () => modSelect.CustomiseButton.Click());
+ AddStep("deselect mod", () => modSelect.SelectMod(testMod));
+ AddAssert("controls hidden", () => modSelect.ModSettingsContainer.Alpha == 0);
+ }
+
+ private class TestModSelectOverlay : ModSelectOverlay
+ {
+ public new Container ModSettingsContainer => base.ModSettingsContainer;
+ public new TriangleButton CustomiseButton => base.CustomiseButton;
+
+ public bool ButtonsLoaded => ModSectionsContainer.Children.All(c => c.ModIconsLoaded);
+
+ public void SelectMod(Mod mod) =>
+ ModSectionsContainer.Children.Single(s => s.ModType == mod.Type)
+ .ButtonsContainer.OfType().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType())).SelectNext(1);
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ foreach (var section in ModSectionsContainer)
+ {
+ if (section.ModType == ModType.Conversion)
+ {
+ section.Mods = new Mod[]
+ {
+ new TestModCustomisable1(),
+ new TestModCustomisable2()
+ };
+ }
+ else
+ section.Mods = Array.Empty();
+ }
+ }
+ }
+
+ private class TestModCustomisable1 : TestModCustomisable
+ {
+ public override string Name => "Customisable Mod 1";
+
+ public override string Acronym => "CM1";
+ }
+
+ private class TestModCustomisable2 : TestModCustomisable
+ {
+ public override string Name => "Customisable Mod 2";
+
+ public override string Acronym => "CM2";
+ }
+
+ private abstract class TestModCustomisable : Mod, IApplicableMod
+ {
+ public override double ScoreMultiplier => 1.0;
+
+ public override ModType Type => ModType.Conversion;
+
+ [SettingSource("Sample float", "Change something for a mod")]
+ public BindableFloat SliderBindable { get; } = new BindableFloat
+ {
+ MinValue = 0,
+ MaxValue = 10,
+ Default = 5,
+ Value = 7
+ };
+
+ [SettingSource("Sample bool", "Clicking this changes a setting")]
+ public BindableBool TickBindable { get; } = new BindableBool();
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs
index 3d39bb7003..7207506ccd 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestScenePopupDialog.cs
@@ -1,9 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Dialog;
namespace osu.Game.Tests.Visual.UserInterface
@@ -11,13 +14,22 @@ namespace osu.Game.Tests.Visual.UserInterface
[TestFixture]
public class TestScenePopupDialog : OsuTestScene
{
+ public override IReadOnlyList RequiredTypes => new[]
+ {
+ typeof(PopupDialogOkButton),
+ typeof(PopupDialogCancelButton),
+ typeof(PopupDialogButton),
+ typeof(DialogButton),
+ };
+
public TestScenePopupDialog()
{
- Add(new TestPopupDialog
- {
- RelativeSizeAxes = Axes.Both,
- State = { Value = Framework.Graphics.Containers.Visibility.Visible },
- });
+ AddStep("new popup", () =>
+ Add(new TestPopupDialog
+ {
+ RelativeSizeAxes = Axes.Both,
+ State = { Value = Framework.Graphics.Containers.Visibility.Visible },
+ }));
}
private class TestPopupDialog : PopupDialog
diff --git a/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs b/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs
index a345f93896..3ff4718b75 100644
--- a/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs
+++ b/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs
@@ -83,88 +83,81 @@ namespace osu.Game.Tournament.Screens.Drawings.Components
};
}
- private ScrollState _scrollState;
+ private ScrollState scrollState;
- private ScrollState scrollState
+ private void setScrollState(ScrollState newstate)
{
- get => _scrollState;
+ if (scrollState == newstate)
+ return;
- set
+ delayedStateChangeDelegate?.Cancel();
+
+ switch (scrollState = newstate)
{
- if (_scrollState == value)
- return;
+ case ScrollState.Scrolling:
+ resetSelected();
- _scrollState = value;
+ OnScrollStarted?.Invoke();
- delayedStateChangeDelegate?.Cancel();
+ speedTo(1000f, 200);
+ tracker.FadeOut(100);
+ break;
- switch (value)
- {
- case ScrollState.Scrolling:
- resetSelected();
+ case ScrollState.Stopping:
+ speedTo(0f, 2000);
+ tracker.FadeIn(200);
- OnScrollStarted?.Invoke();
+ delayedStateChangeDelegate = Scheduler.AddDelayed(() => setScrollState(ScrollState.Stopped), 2300);
+ break;
- speedTo(1000f, 200);
- tracker.FadeOut(100);
+ case ScrollState.Stopped:
+ // Find closest to center
+ if (!Children.Any())
break;
- case ScrollState.Stopping:
- speedTo(0f, 2000);
- tracker.FadeIn(200);
+ ScrollingTeam closest = null;
- delayedStateChangeDelegate = Scheduler.AddDelayed(() => scrollState = ScrollState.Stopped, 2300);
- break;
+ foreach (var c in Children)
+ {
+ if (!(c is ScrollingTeam stc))
+ continue;
- case ScrollState.Stopped:
- // Find closest to center
- if (!Children.Any())
- break;
-
- ScrollingTeam closest = null;
-
- foreach (var c in Children)
+ if (closest == null)
{
- if (!(c is ScrollingTeam stc))
- continue;
-
- if (closest == null)
- {
- closest = stc;
- continue;
- }
-
- float o = Math.Abs(c.Position.X + c.DrawWidth / 2f - DrawWidth / 2f);
- float lastOffset = Math.Abs(closest.Position.X + closest.DrawWidth / 2f - DrawWidth / 2f);
-
- if (o < lastOffset)
- closest = stc;
+ closest = stc;
+ continue;
}
- Trace.Assert(closest != null, "closest != null");
+ float o = Math.Abs(c.Position.X + c.DrawWidth / 2f - DrawWidth / 2f);
+ float lastOffset = Math.Abs(closest.Position.X + closest.DrawWidth / 2f - DrawWidth / 2f);
- // ReSharper disable once PossibleNullReferenceException
- offset += DrawWidth / 2f - (closest.Position.X + closest.DrawWidth / 2f);
+ if (o < lastOffset)
+ closest = stc;
+ }
- ScrollingTeam st = closest;
+ Trace.Assert(closest != null, "closest != null");
- availableTeams.RemoveAll(at => at == st.Team);
+ // ReSharper disable once PossibleNullReferenceException
+ offset += DrawWidth / 2f - (closest.Position.X + closest.DrawWidth / 2f);
- st.Selected = true;
- OnSelected?.Invoke(st.Team);
+ ScrollingTeam st = closest;
- delayedStateChangeDelegate = Scheduler.AddDelayed(() => scrollState = ScrollState.Idle, 10000);
- break;
+ availableTeams.RemoveAll(at => at == st.Team);
- case ScrollState.Idle:
- resetSelected();
+ st.Selected = true;
+ OnSelected?.Invoke(st.Team);
- OnScrollStarted?.Invoke();
+ delayedStateChangeDelegate = Scheduler.AddDelayed(() => setScrollState(ScrollState.Idle), 10000);
+ break;
- speedTo(40f, 200);
- tracker.FadeOut(100);
- break;
- }
+ case ScrollState.Idle:
+ resetSelected();
+
+ OnScrollStarted?.Invoke();
+
+ speedTo(40f, 200);
+ tracker.FadeOut(100);
+ break;
}
}
@@ -176,7 +169,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components
availableTeams.Add(team);
RemoveAll(c => c is ScrollingTeam);
- scrollState = ScrollState.Idle;
+ setScrollState(ScrollState.Idle);
}
public void AddTeams(IEnumerable teams)
@@ -192,7 +185,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components
{
availableTeams.Clear();
RemoveAll(c => c is ScrollingTeam);
- scrollState = ScrollState.Idle;
+ setScrollState(ScrollState.Idle);
}
public void RemoveTeam(TournamentTeam team)
@@ -217,7 +210,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components
if (availableTeams.Count == 0)
return;
- scrollState = ScrollState.Scrolling;
+ setScrollState(ScrollState.Scrolling);
}
public void StopScrolling()
@@ -232,13 +225,13 @@ namespace osu.Game.Tournament.Screens.Drawings.Components
return;
}
- scrollState = ScrollState.Stopping;
+ setScrollState(ScrollState.Stopping);
}
protected override void LoadComplete()
{
base.LoadComplete();
- scrollState = ScrollState.Idle;
+ setScrollState(ScrollState.Idle);
}
protected override void UpdateAfterChildren()
@@ -305,7 +298,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components
private void speedTo(float value, double duration = 0, Easing easing = Easing.None) =>
this.TransformTo(nameof(speed), value, duration, easing);
- private enum ScrollState
+ protected enum ScrollState
{
None,
Idle,
diff --git a/osu.Game.Tournament/osu.Game.Tournament.csproj b/osu.Game.Tournament/osu.Game.Tournament.csproj
index 8e881fdd9c..9cce40c9d3 100644
--- a/osu.Game.Tournament/osu.Game.Tournament.csproj
+++ b/osu.Game.Tournament/osu.Game.Tournament.csproj
@@ -9,6 +9,6 @@
-
+
\ No newline at end of file
diff --git a/osu.Game/Configuration/DatabasedConfigManager.cs b/osu.Game/Configuration/DatabasedConfigManager.cs
index 1ef4c2527a..b3783b45a8 100644
--- a/osu.Game/Configuration/DatabasedConfigManager.cs
+++ b/osu.Game/Configuration/DatabasedConfigManager.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
@@ -9,8 +10,8 @@ using osu.Game.Rulesets;
namespace osu.Game.Configuration
{
- public abstract class DatabasedConfigManager : ConfigManager
- where T : struct
+ public abstract class DatabasedConfigManager : ConfigManager
+ where TLookup : struct, Enum
{
private readonly SettingsStore settings;
@@ -53,7 +54,7 @@ namespace osu.Game.Configuration
private readonly List dirtySettings = new List();
- protected override void AddBindable(T lookup, Bindable bindable)
+ protected override void AddBindable(TLookup lookup, Bindable bindable)
{
base.AddBindable(lookup, bindable);
diff --git a/osu.Game/Configuration/InMemoryConfigManager.cs b/osu.Game/Configuration/InMemoryConfigManager.cs
index b0dc6b0e9c..ccf697f680 100644
--- a/osu.Game/Configuration/InMemoryConfigManager.cs
+++ b/osu.Game/Configuration/InMemoryConfigManager.cs
@@ -1,12 +1,13 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osu.Framework.Configuration;
namespace osu.Game.Configuration
{
- public class InMemoryConfigManager : ConfigManager
- where T : struct
+ public class InMemoryConfigManager : ConfigManager
+ where TLookup : struct, Enum
{
public InMemoryConfigManager()
{
diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs
new file mode 100644
index 0000000000..056fa8bcc0
--- /dev/null
+++ b/osu.Game/Configuration/SettingSourceAttribute.cs
@@ -0,0 +1,110 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using JetBrains.Annotations;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.UserInterface;
+using osu.Game.Overlays.Settings;
+
+namespace osu.Game.Configuration
+{
+ ///
+ /// An attribute to mark a bindable as being exposed to the user via settings controls.
+ /// Can be used in conjunction with to automatically create UI controls.
+ ///
+ [MeansImplicitUse]
+ [AttributeUsage(AttributeTargets.Property)]
+ public class SettingSourceAttribute : Attribute
+ {
+ public string Label { get; }
+
+ public string Description { get; }
+
+ public SettingSourceAttribute(string label, string description = null)
+ {
+ Label = label ?? string.Empty;
+ Description = description ?? string.Empty;
+ }
+ }
+
+ public static class SettingSourceExtensions
+ {
+ public static IEnumerable CreateSettingsControls(this object obj)
+ {
+ foreach (var property in obj.GetType().GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance))
+ {
+ var attr = property.GetCustomAttribute(true);
+
+ if (attr == null)
+ continue;
+
+ var prop = property.GetValue(obj);
+
+ switch (prop)
+ {
+ case BindableNumber bNumber:
+ yield return new SettingsSlider
+ {
+ LabelText = attr.Label,
+ Bindable = bNumber
+ };
+
+ break;
+
+ case BindableNumber bNumber:
+ yield return new SettingsSlider
+ {
+ LabelText = attr.Label,
+ Bindable = bNumber
+ };
+
+ break;
+
+ case BindableNumber bNumber:
+ yield return new SettingsSlider
+ {
+ LabelText = attr.Label,
+ Bindable = bNumber
+ };
+
+ break;
+
+ case Bindable bBool:
+ yield return new SettingsCheckbox
+ {
+ LabelText = attr.Label,
+ Bindable = bBool
+ };
+
+ break;
+
+ case Bindable bString:
+ yield return new SettingsTextBox
+ {
+ LabelText = attr.Label,
+ Bindable = bString
+ };
+
+ break;
+
+ case IBindable bindable:
+ var dropdownType = typeof(SettingsEnumDropdown<>).MakeGenericType(bindable.GetType().GetGenericArguments()[0]);
+ var dropdown = (Drawable)Activator.CreateInstance(dropdownType);
+
+ dropdown.GetType().GetProperty(nameof(IHasCurrentValue