diff --git a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
index 936808f38b..52b728a115 100644
--- a/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
+++ b/Templates/Rulesets/ruleset-empty/osu.Game.Rulesets.EmptyFreeform.Tests/osu.Game.Rulesets.EmptyFreeform.Tests.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index 35e7742172..95b96adab0 100644
--- a/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
index c1044965b5..d12403016d 100644
--- a/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-empty/osu.Game.Rulesets.EmptyScrolling.Tests/osu.Game.Rulesets.EmptyScrolling.Tests.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
index 35e7742172..95b96adab0 100644
--- a/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
+++ b/Templates/Rulesets/ruleset-scrolling-example/osu.Game.Rulesets.Pippidon.Tests/osu.Game.Rulesets.Pippidon.Tests.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/osu.Android.props b/osu.Android.props
index 8711ceec64..b6ddeeb41a 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,11 +51,11 @@
-
+
-
+
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index 3f926ed45a..1f4544098b 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -27,7 +27,7 @@
-
+
diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
index d62d422f33..f47b069373 100644
--- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
+++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index c9db824615..5a2e8e0bf0 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -3,7 +3,7 @@
-
+
WinExe
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index 0d7b03d830..be51dc0e4c 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -3,7 +3,7 @@
-
+
WinExe
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs
new file mode 100644
index 0000000000..800e6c0711
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuComposerSelection.cs
@@ -0,0 +1,117 @@
+// 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 NUnit.Framework;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Screens.Edit.Compose.Components;
+using osu.Game.Tests.Beatmaps;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Osu.Tests.Editor
+{
+ [TestFixture]
+ public class TestSceneOsuComposerSelection : TestSceneOsuEditor
+ {
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
+
+ [Test]
+ public void TestContextMenuShownCorrectlyForSelectedSlider()
+ {
+ var slider = new Slider
+ {
+ StartTime = 0,
+ Position = new Vector2(100, 100),
+ Path = new SliderPath
+ {
+ ControlPoints =
+ {
+ new PathControlPoint(),
+ new PathControlPoint(new Vector2(100))
+ }
+ }
+ };
+ AddStep("add slider", () => EditorBeatmap.Add(slider));
+
+ moveMouseToObject(() => slider);
+ AddStep("left click", () => InputManager.Click(MouseButton.Left));
+ AddUntilStep("slider selected", () => EditorBeatmap.SelectedHitObjects.Single() == slider);
+
+ AddStep("move mouse to centre", () => InputManager.MoveMouseTo(blueprintContainer.ChildrenOfType().Single().ScreenSpaceDrawQuad.Centre));
+ AddStep("right click", () => InputManager.Click(MouseButton.Right));
+ AddUntilStep("context menu is visible", () => contextMenuContainer.ChildrenOfType().Single().State == MenuState.Open);
+ }
+
+ [Test]
+ public void TestSelectionIncludingSliderPreservedOnClick()
+ {
+ var firstSlider = new Slider
+ {
+ StartTime = 0,
+ Position = new Vector2(0, 0),
+ Path = new SliderPath
+ {
+ ControlPoints =
+ {
+ new PathControlPoint(),
+ new PathControlPoint(new Vector2(100))
+ }
+ }
+ };
+ var secondSlider = new Slider
+ {
+ StartTime = 1000,
+ Position = new Vector2(100, 100),
+ Path = new SliderPath
+ {
+ ControlPoints =
+ {
+ new PathControlPoint(),
+ new PathControlPoint(new Vector2(100, -100))
+ }
+ }
+ };
+ var hitCircle = new HitCircle
+ {
+ StartTime = 200,
+ Position = new Vector2(300, 0)
+ };
+
+ AddStep("add objects", () => EditorBeatmap.AddRange(new HitObject[] { firstSlider, secondSlider, hitCircle }));
+ AddStep("select last 2 objects", () => EditorBeatmap.SelectedHitObjects.AddRange(new HitObject[] { secondSlider, hitCircle }));
+
+ moveMouseToObject(() => secondSlider);
+ AddStep("click left mouse", () => InputManager.Click(MouseButton.Left));
+ AddAssert("selection preserved", () => EditorBeatmap.SelectedHitObjects.Count == 2);
+ }
+
+ private ComposeBlueprintContainer blueprintContainer
+ => Editor.ChildrenOfType().First();
+
+ private ContextMenuContainer contextMenuContainer
+ => Editor.ChildrenOfType().First();
+
+ private void moveMouseToObject(Func targetFunc)
+ {
+ AddStep("move mouse to object", () =>
+ {
+ var pos = blueprintContainer.SelectionBlueprints
+ .First(s => s.Item == targetFunc())
+ .ChildrenOfType()
+ .First().ScreenSpaceDrawQuad.Centre;
+
+ InputManager.MoveMouseTo(pos);
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index 1eb1c85d93..c10c3ffb15 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -4,7 +4,7 @@
-
+
WinExe
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index 36ee7c2460..3718f0c6bc 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -105,8 +105,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
return true;
}
- private bool hasSingleObjectSelected => selectedObjects.Count == 1;
-
protected override void Update()
{
base.Update();
@@ -119,10 +117,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
updateVisualDefinition();
- // In the case more than a single object is selected, block hover from arriving at sliders behind this one.
- // Without doing this, the path visualisers of potentially hundreds of sliders will render, which is not only
- // visually noisy but also functionally useless.
- return !hasSingleObjectSelected;
+ return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
@@ -147,8 +142,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updateVisualDefinition()
{
- // To reduce overhead of drawing these blueprints, only add extra detail when hovered or when only this slider is selected.
- if (IsSelected && (hasSingleObjectSelected || IsHovered))
+ // To reduce overhead of drawing these blueprints, only add extra detail when only this slider is selected.
+ if (IsSelected && selectedObjects.Count < 2)
{
if (ControlPointVisualiser == null)
{
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs
index 4669b5b913..f99d4275bd 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerDisc.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
@@ -16,7 +15,6 @@ using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osuTK;
-using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
@@ -52,6 +50,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
private Container centre = null!;
private CircularContainer fill = null!;
+ private Container ticksContainer = null!;
+ private ArgonSpinnerTicks ticks = null!;
+
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableHitObject)
{
@@ -70,41 +71,85 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
- fill = new CircularContainer
+ new Container
{
- Name = @"Fill",
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
- Masking = true,
- EdgeEffect = new EdgeEffectParameters
+ Padding = new MarginPadding(8f),
+ Children = new[]
{
- Type = EdgeEffectType.Shadow,
- Colour = Colour4.FromHex("FC618F").Opacity(1f),
- Radius = 40,
+ fill = new CircularContainer
+ {
+ Name = @"Fill",
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Shadow,
+ Colour = Colour4.FromHex("FC618F").Opacity(1f),
+ Radius = 40,
+ },
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0f,
+ AlwaysPresent = true,
+ }
+ },
+ ticksContainer = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Child = ticks = new ArgonSpinnerTicks(),
+ }
},
- Child = new Box
- {
- RelativeSizeAxes = Axes.Both,
- Alpha = 0f,
- AlwaysPresent = true,
- }
},
- new CircularContainer
+ new Container
{
Name = @"Ring",
Masking = true,
- BorderColour = Color4.White,
- BorderThickness = 5,
RelativeSizeAxes = Axes.Both,
- Child = new Box
+ Padding = new MarginPadding(8f),
+ Children = new[]
{
- RelativeSizeAxes = Axes.Both,
- Alpha = 0,
- AlwaysPresent = true,
+ new ArgonSpinnerRingArc
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Name = "Top Arc",
+ },
+ new ArgonSpinnerRingArc
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Name = "Bottom Arc",
+ Scale = new Vector2(1, -1),
+ },
+ }
+ },
+ new Container
+ {
+ Name = @"Sides",
+ RelativeSizeAxes = Axes.Both,
+ Children = new[]
+ {
+ new ArgonSpinnerProgressArc
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Name = "Left Bar"
+ },
+ new ArgonSpinnerProgressArc
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Name = "Right Bar",
+ Scale = new Vector2(-1, 1),
+ },
}
},
- new ArgonSpinnerTicks(),
}
},
centre = new Container
@@ -138,6 +183,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
}
+ private float trackingElementInterpolation;
+
protected override void Update()
{
base.Update();
@@ -157,17 +204,18 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
}
else
{
- fill.Alpha = (float)Interpolation.Damp(fill.Alpha, drawableSpinner.RotationTracker.Tracking ? tracking_alpha : idle_alpha, 0.98f, (float)Math.Abs(Clock.ElapsedFrameTime));
- }
+ trackingElementInterpolation =
+ (float)Interpolation.Damp(trackingElementInterpolation, drawableSpinner.RotationTracker.Tracking ? 1 : 0, 0.985f, (float)Math.Abs(Clock.ElapsedFrameTime));
- if (centre.Width == idle_centre_size && drawableSpinner.Result?.TimeStarted != null)
- updateCentrePieceSize();
+ fill.Alpha = trackingElementInterpolation * (tracking_alpha - idle_alpha) + idle_alpha;
+ centre.Size = new Vector2(trackingElementInterpolation * (tracking_centre_size - idle_centre_size) + idle_centre_size);
+ }
const float initial_fill_scale = 0.1f;
float targetScale = initial_fill_scale + (0.98f - initial_fill_scale) * drawableSpinner.Progress;
fill.Scale = new Vector2((float)Interpolation.Lerp(fill.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1)));
- disc.Rotation = drawableSpinner.RotationTracker.Rotation;
+ ticks.Rotation = drawableSpinner.RotationTracker.Rotation;
}
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
@@ -180,35 +228,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
{
this.ScaleTo(initial_scale);
- this.RotateTo(0);
+ ticksContainer.RotateTo(0);
+ centre.ScaleTo(0);
+ disc.ScaleTo(0);
using (BeginDelayedSequence(spinner.TimePreempt / 2))
{
// constant ambient rotation to give the spinner "spinning" character.
- this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration);
+ ticksContainer.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration);
}
- using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset))
- {
- switch (state)
- {
- case ArmedState.Hit:
- this.ScaleTo(initial_scale * 1.2f, 320, Easing.Out);
- this.RotateTo(Rotation + 180, 320);
- break;
-
- case ArmedState.Miss:
- this.ScaleTo(initial_scale * 0.8f, 320, Easing.In);
- break;
- }
- }
- }
-
- using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
- {
- centre.ScaleTo(0);
- disc.ScaleTo(0);
-
using (BeginDelayedSequence(spinner.TimePreempt / 2))
{
centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint);
@@ -220,20 +249,22 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
disc.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint);
}
}
+
+ using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset))
+ {
+ switch (state)
+ {
+ case ArmedState.Hit:
+ disc.ScaleTo(initial_scale * 1.2f, 320, Easing.Out);
+ ticksContainer.RotateTo(ticksContainer.Rotation + 180, 320);
+ break;
+
+ case ArmedState.Miss:
+ disc.ScaleTo(initial_scale * 0.8f, 320, Easing.In);
+ break;
+ }
+ }
}
-
- if (drawableSpinner.Result?.TimeStarted != null)
- updateCentrePieceSize();
- }
-
- private void updateCentrePieceSize()
- {
- Debug.Assert(drawableSpinner.Result?.TimeStarted != null);
-
- Spinner spinner = drawableSpinner.HitObject;
-
- using (BeginAbsoluteSequence(drawableSpinner.Result.TimeStarted.Value))
- centre.ResizeTo(new Vector2(tracking_centre_size), spinner.TimePreempt / 2, Easing.OutQuint);
}
protected override void Dispose(bool isDisposing)
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerProgressArc.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerProgressArc.cs
new file mode 100644
index 0000000000..e998f55755
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerProgressArc.cs
@@ -0,0 +1,71 @@
+// 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.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonSpinnerProgressArc : CompositeDrawable
+ {
+ private const float arc_fill = 0.15f;
+ private const float arc_radius = 0.12f;
+
+ private CircularProgress fill = null!;
+
+ private DrawableSpinner spinner = null!;
+
+ private CircularProgress background = null!;
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject drawableHitObject)
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ spinner = (DrawableSpinner)drawableHitObject;
+
+ InternalChildren = new Drawable[]
+ {
+ background = new CircularProgress
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Colour = Color4.White.Opacity(0.25f),
+ RelativeSizeAxes = Axes.Both,
+ Current = { Value = arc_fill },
+ Rotation = 90 - arc_fill * 180,
+ InnerRadius = arc_radius,
+ RoundedCaps = true,
+ },
+ fill = new CircularProgress
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ InnerRadius = arc_radius,
+ RoundedCaps = true,
+ }
+ };
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ background.Alpha = spinner.Progress >= 1 ? 0 : 1;
+
+ fill.Alpha = (float)Interpolation.DampContinuously(fill.Alpha, spinner.Progress > 0 && spinner.Progress < 1 ? 1 : 0, 40f, (float)Math.Abs(Time.Elapsed));
+ fill.Current.Value = (float)Interpolation.DampContinuously(fill.Current.Value, spinner.Progress >= 1 ? 0 : arc_fill * spinner.Progress, 40f, (float)Math.Abs(Time.Elapsed));
+
+ fill.Rotation = (float)(90 - fill.Current.Value * 180);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerRingArc.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerRingArc.cs
new file mode 100644
index 0000000000..57fb57a09e
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonSpinnerRingArc.cs
@@ -0,0 +1,53 @@
+// 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.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Osu.Skinning.Argon
+{
+ public class ArgonSpinnerRingArc : CompositeDrawable
+ {
+ private const float arc_fill = 0.31f;
+ private const float arc_fill_complete = 0.50f;
+
+ private const float arc_radius = 0.02f;
+
+ private DrawableSpinner spinner = null!;
+ private CircularProgress fill = null!;
+
+ [BackgroundDependencyLoader]
+ private void load(DrawableHitObject drawableHitObject)
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ spinner = (DrawableSpinner)drawableHitObject;
+ InternalChild = fill = new CircularProgress
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Current = { Value = arc_fill },
+ Rotation = -arc_fill * 180,
+ InnerRadius = arc_radius,
+ RoundedCaps = true,
+ };
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ fill.Current.Value = (float)Interpolation.DampContinuously(fill.Current.Value, spinner.Progress >= 1 ? arc_fill_complete : arc_fill, 40f, (float)Math.Abs(Time.Elapsed));
+ fill.InnerRadius = (float)Interpolation.DampContinuously(fill.InnerRadius, spinner.Progress >= 1 ? arc_radius * 2.2f : arc_radius, 40f, (float)Math.Abs(Time.Elapsed));
+
+ fill.Rotation = (float)(-fill.Current.Value * 180);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs
index 8e9c487c2f..eb2b6c1d74 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs
@@ -30,23 +30,24 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
Origin = Anchor.Centre,
}));
- AddStep("Rim hit", () => SetContents(_ => new DrawableHit(createHitAtCurrentTime())
+ AddStep("Rim hit", () => SetContents(_ => new DrawableHit(createHitAtCurrentTime(rim: true))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}));
- AddStep("Rim hit (strong)", () => SetContents(_ => new DrawableHit(createHitAtCurrentTime(true))
+ AddStep("Rim hit (strong)", () => SetContents(_ => new DrawableHit(createHitAtCurrentTime(true, true))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}));
}
- private Hit createHitAtCurrentTime(bool strong = false)
+ private Hit createHitAtCurrentTime(bool strong = false, bool rim = false)
{
var hit = new Hit
{
+ Type = rim ? HitType.Rim : HitType.Centre,
IsStrong = strong,
StartTime = Time.Current + 3000,
};
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs
index f87e0355ad..0ddc607336 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs
@@ -13,6 +13,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI;
+using osu.Game.Screens.Ranking;
namespace osu.Game.Rulesets.Taiko.Tests.Skinning
{
@@ -49,11 +50,19 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
// the hit needs to be added to hierarchy in order for nested objects to be created correctly.
// setting zero alpha is supposed to prevent the test from looking broken.
hit.With(h => h.Alpha = 0),
- new HitExplosion(hit.Type)
+
+ new AspectContainer
{
+ RelativeSizeAxes = Axes.X,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- }.With(explosion => explosion.Apply(hit))
+ Child =
+ new HitExplosion(hit.Type)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ }.With(explosion => explosion.Apply(hit))
+ }
}
};
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index 38e61f5624..6af1beff69 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -3,7 +3,7 @@
-
+
WinExe
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/CentreHitCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/CentreHitCirclePiece.cs
index b91d5cfe8d..958f4b3a17 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Default/CentreHitCirclePiece.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Default/CentreHitCirclePiece.cs
@@ -41,12 +41,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
Children = new[]
{
- new CircularContainer
- {
- RelativeSizeAxes = Axes.Both,
- Masking = true,
- Children = new[] { new Box { RelativeSizeAxes = Axes.Both } }
- }
+ new Circle { RelativeSizeAxes = Axes.Both }
};
}
}
diff --git a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultHitExplosion.cs
similarity index 87%
rename from osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs
rename to osu.Game.Rulesets.Taiko/Skinning/Default/DefaultHitExplosion.cs
index 687c8f788f..b7ba76effa 100644
--- a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultHitExplosion.cs
@@ -1,9 +1,6 @@
// 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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -12,19 +9,19 @@ using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.UI;
using osuTK.Graphics;
-namespace osu.Game.Rulesets.Taiko.UI
+namespace osu.Game.Rulesets.Taiko.Skinning.Default
{
internal class DefaultHitExplosion : CircularContainer, IAnimatableHitExplosion
{
private readonly HitResult result;
- [CanBeNull]
- private Box body;
+ private Box? body;
[Resolved]
- private OsuColour colours { get; set; }
+ private OsuColour colours { get; set; } = null!;
public DefaultHitExplosion(HitResult result)
{
@@ -58,7 +55,7 @@ namespace osu.Game.Rulesets.Taiko.UI
updateColour();
}
- private void updateColour([CanBeNull] DrawableHitObject judgedObject = null)
+ private void updateColour(DrawableHitObject? judgedObject = null)
{
if (body == null)
return;
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultInputDrum.cs
new file mode 100644
index 0000000000..fa60d209e7
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultInputDrum.cs
@@ -0,0 +1,181 @@
+// 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;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
+using osu.Game.Graphics;
+using osu.Game.Screens.Ranking;
+using osuTK;
+
+namespace osu.Game.Rulesets.Taiko.Skinning.Default
+{
+ public class DefaultInputDrum : AspectContainer
+ {
+ public DefaultInputDrum()
+ {
+ RelativeSizeAxes = Axes.Y;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ const float middle_split = 0.025f;
+
+ InternalChild = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Scale = new Vector2(0.9f),
+ Children = new[]
+ {
+ new TaikoHalfDrum(false)
+ {
+ Name = "Left Half",
+ Anchor = Anchor.Centre,
+ Origin = Anchor.CentreRight,
+ RelativeSizeAxes = Axes.Both,
+ RelativePositionAxes = Axes.X,
+ X = -middle_split / 2,
+ RimAction = TaikoAction.LeftRim,
+ CentreAction = TaikoAction.LeftCentre
+ },
+ new TaikoHalfDrum(true)
+ {
+ Name = "Right Half",
+ Anchor = Anchor.Centre,
+ Origin = Anchor.CentreLeft,
+ RelativeSizeAxes = Axes.Both,
+ RelativePositionAxes = Axes.X,
+ X = middle_split / 2,
+ RimAction = TaikoAction.RightRim,
+ CentreAction = TaikoAction.RightCentre
+ }
+ }
+ };
+ }
+
+ ///
+ /// A half-drum. Contains one centre and one rim hit.
+ ///
+ private class TaikoHalfDrum : Container, IKeyBindingHandler
+ {
+ ///
+ /// The key to be used for the rim of the half-drum.
+ ///
+ public TaikoAction RimAction;
+
+ ///
+ /// The key to be used for the centre of the half-drum.
+ ///
+ public TaikoAction CentreAction;
+
+ private readonly Sprite rim;
+ private readonly Sprite rimHit;
+ private readonly Sprite centre;
+ private readonly Sprite centreHit;
+
+ public TaikoHalfDrum(bool flipped)
+ {
+ Masking = true;
+
+ Children = new Drawable[]
+ {
+ rim = new Sprite
+ {
+ Anchor = flipped ? Anchor.CentreLeft : Anchor.CentreRight,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both
+ },
+ rimHit = new Sprite
+ {
+ Anchor = flipped ? Anchor.CentreLeft : Anchor.CentreRight,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ Blending = BlendingParameters.Additive,
+ },
+ centre = new Sprite
+ {
+ Anchor = flipped ? Anchor.CentreLeft : Anchor.CentreRight,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(0.7f)
+ },
+ centreHit = new Sprite
+ {
+ Anchor = flipped ? Anchor.CentreLeft : Anchor.CentreRight,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(0.7f),
+ Alpha = 0,
+ Blending = BlendingParameters.Additive
+ }
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(TextureStore textures, OsuColour colours)
+ {
+ rim.Texture = textures.Get(@"Gameplay/taiko/taiko-drum-outer");
+ rimHit.Texture = textures.Get(@"Gameplay/taiko/taiko-drum-outer-hit");
+ centre.Texture = textures.Get(@"Gameplay/taiko/taiko-drum-inner");
+ centreHit.Texture = textures.Get(@"Gameplay/taiko/taiko-drum-inner-hit");
+
+ rimHit.Colour = colours.Blue;
+ centreHit.Colour = colours.Pink;
+ }
+
+ public bool OnPressed(KeyBindingPressEvent e)
+ {
+ Drawable target = null;
+ Drawable back = null;
+
+ if (e.Action == CentreAction)
+ {
+ target = centreHit;
+ back = centre;
+ }
+ else if (e.Action == RimAction)
+ {
+ target = rimHit;
+ back = rim;
+ }
+
+ if (target != null)
+ {
+ const float scale_amount = 0.05f;
+ const float alpha_amount = 0.5f;
+
+ const float down_time = 40;
+ const float up_time = 1000;
+
+ back.ScaleTo(target.Scale.X - scale_amount, down_time, Easing.OutQuint)
+ .Then()
+ .ScaleTo(1, up_time, Easing.OutQuint);
+
+ target.Animate(
+ t => t.ScaleTo(target.Scale.X - scale_amount, down_time, Easing.OutQuint),
+ t => t.FadeTo(Math.Min(target.Alpha + alpha_amount, 1), down_time, Easing.OutQuint)
+ ).Then(
+ t => t.ScaleTo(1, up_time, Easing.OutQuint),
+ t => t.FadeOut(up_time, Easing.OutQuint)
+ );
+ }
+
+ return false;
+ }
+
+ public void OnReleased(KeyBindingReleaseEvent e)
+ {
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/UI/DefaultKiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultKiaiHitExplosion.cs
similarity index 96%
rename from osu.Game.Rulesets.Taiko/UI/DefaultKiaiHitExplosion.cs
rename to osu.Game.Rulesets.Taiko/Skinning/Default/DefaultKiaiHitExplosion.cs
index e91475d87b..ae68d63d97 100644
--- a/osu.Game.Rulesets.Taiko/UI/DefaultKiaiHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Default/DefaultKiaiHitExplosion.cs
@@ -1,9 +1,6 @@
// 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 osuTK;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -11,8 +8,9 @@ using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Rulesets.Taiko.Objects;
+using osuTK;
-namespace osu.Game.Rulesets.Taiko.UI
+namespace osu.Game.Rulesets.Taiko.Skinning.Default
{
public class DefaultKiaiHitExplosion : CircularContainer
{
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/ElongatedCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/ElongatedCirclePiece.cs
index ba2679fe97..210841bca0 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Default/ElongatedCirclePiece.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Default/ElongatedCirclePiece.cs
@@ -1,8 +1,6 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics;
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/RimHitCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/RimHitCirclePiece.cs
index 63269f1267..c6165495d8 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Default/RimHitCirclePiece.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Default/RimHitCirclePiece.cs
@@ -1,8 +1,6 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/SwellSymbolPiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/SwellSymbolPiece.cs
index d19dc4c887..2f59cac3ff 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Default/SwellSymbolPiece.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Default/SwellSymbolPiece.cs
@@ -1,8 +1,6 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/TickPiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/TickPiece.cs
index 7d3268f777..09c8243aac 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Default/TickPiece.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Default/TickPiece.cs
@@ -1,8 +1,6 @@
// 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 osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyBarLine.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyBarLine.cs
index 97e0a340dd..2b528ae8ce 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyBarLine.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyBarLine.cs
@@ -1,8 +1,6 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs
index 6bbeb0ed4c..6b2576a564 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs
@@ -1,8 +1,6 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
@@ -19,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
public class LegacyCirclePiece : CompositeDrawable, IHasAccentColour
{
- private Drawable backgroundLayer;
+ private Drawable backgroundLayer = null!;
// required for editor blueprints (not sure why these circle pieces are zero size).
public override Quad ScreenSpaceDrawQuad => backgroundLayer.ScreenSpaceDrawQuad;
@@ -32,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
[BackgroundDependencyLoader]
private void load(ISkinSource skin, DrawableHitObject drawableHitObject)
{
- Drawable getDrawableFor(string lookup)
+ Drawable? getDrawableFor(string lookup)
{
const string normal_hit = "taikohit";
const string big_hit = "taikobig";
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs
index 040d8ff965..1249231d92 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs
@@ -1,8 +1,6 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -28,11 +26,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
}
}
- private LegacyCirclePiece headCircle;
+ private LegacyCirclePiece headCircle = null!;
- private Sprite body;
+ private Sprite body = null!;
- private Sprite tailCircle;
+ private Sprite tailCircle = null!;
public LegacyDrumRoll()
{
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHit.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHit.cs
index b4277f86bb..d93317f0e2 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHit.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHit.cs
@@ -1,8 +1,6 @@
// 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 osu.Framework.Allocation;
using osu.Game.Skinning;
using osuTK.Graphics;
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs
index 87ed2e2e60..ff1546381b 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs
@@ -1,9 +1,6 @@
// 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 JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
@@ -17,8 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
private readonly Drawable sprite;
- [CanBeNull]
- private readonly Drawable strongSprite;
+ private readonly Drawable? strongSprite;
///
/// Creates a new legacy hit explosion.
@@ -29,7 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
///
/// The normal legacy explosion sprite.
/// The strong legacy explosion sprite.
- public LegacyHitExplosion(Drawable sprite, [CanBeNull] Drawable strongSprite = null)
+ public LegacyHitExplosion(Drawable sprite, Drawable? strongSprite = null)
{
this.sprite = sprite;
this.strongSprite = strongSprite;
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs
index 101f70b97a..0abb365750 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs
@@ -1,8 +1,6 @@
// 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;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -20,9 +18,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
///
internal class LegacyInputDrum : Container
{
- private Container content;
- private LegacyHalfDrum left;
- private LegacyHalfDrum right;
+ private Container content = null!;
+ private LegacyHalfDrum left = null!;
+ private LegacyHalfDrum right = null!;
public LegacyInputDrum()
{
@@ -142,7 +140,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
public bool OnPressed(KeyBindingPressEvent e)
{
- Drawable target = null;
+ Drawable? target = null;
if (e.Action == CentreAction)
{
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyTaikoScroller.cs
index bd4a2f8935..4a2426bff5 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyTaikoScroller.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyTaikoScroller.cs
@@ -1,8 +1,6 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
@@ -27,7 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
}
[BackgroundDependencyLoader(true)]
- private void load(GameplayState gameplayState)
+ private void load(GameplayState? gameplayState)
{
if (gameplayState != null)
((IBindable)LastResult).BindTo(gameplayState.LastJudgementResult);
@@ -91,8 +89,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
private class ScrollerSprite : CompositeDrawable
{
- private Sprite passingSprite;
- private Sprite failingSprite;
+ private Sprite passingSprite = null!;
+ private Sprite failingSprite = null!;
private bool passing = true;
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs
index a48cdf47f6..21102f6eec 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs
@@ -1,8 +1,6 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -15,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
public class TaikoLegacyHitTarget : CompositeDrawable
{
- private Container content;
+ private Container content = null!;
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyPlayfieldBackgroundRight.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyPlayfieldBackgroundRight.cs
index f425a410a4..3186f615a7 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyPlayfieldBackgroundRight.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyPlayfieldBackgroundRight.cs
@@ -1,8 +1,6 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics;
@@ -16,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
public class TaikoLegacyPlayfieldBackgroundRight : BeatSyncedContainer
{
- private Sprite kiai;
+ private Sprite kiai = null!;
private bool kiaiDisplayed;
diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponent.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponent.cs
index 63314a6822..30bfb605aa 100644
--- a/osu.Game.Rulesets.Taiko/TaikoSkinComponent.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponent.cs
@@ -1,8 +1,6 @@
// 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 osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko
diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
index d231dc7e4f..bf48898dd2 100644
--- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
+++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Rulesets.Taiko
{
public enum TaikoSkinComponents
diff --git a/osu.Game.Rulesets.Taiko/UI/BarLinePlayfield.cs b/osu.Game.Rulesets.Taiko/UI/BarLinePlayfield.cs
index 071808a044..cb878e8ea0 100644
--- a/osu.Game.Rulesets.Taiko/UI/BarLinePlayfield.cs
+++ b/osu.Game.Rulesets.Taiko/UI/BarLinePlayfield.cs
@@ -1,8 +1,6 @@
// 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 osu.Framework.Allocation;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
index 264e4db54e..876fa207bf 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs
@@ -1,8 +1,6 @@
// 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 osu.Framework.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs
index 8bedca19d8..dd0b61cdf5 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs
@@ -1,8 +1,6 @@
// 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.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
@@ -24,7 +22,8 @@ namespace osu.Game.Rulesets.Taiko.UI
public readonly Bindable LastResult;
private readonly Dictionary animations;
- private TaikoMascotAnimation currentAnimation;
+
+ private TaikoMascotAnimation? currentAnimation;
private bool lastObjectHit = true;
private bool kiaiMode;
@@ -40,7 +39,7 @@ namespace osu.Game.Rulesets.Taiko.UI
}
[BackgroundDependencyLoader(true)]
- private void load(GameplayState gameplayState)
+ private void load(GameplayState? gameplayState)
{
InternalChildren = new[]
{
diff --git a/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs b/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs
index e0d5a3c680..ae37840825 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs
@@ -1,8 +1,6 @@
// 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 osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
diff --git a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs
index ef5bd1d7f0..3279d128d3 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrumSampleTriggerSource.cs
@@ -1,8 +1,6 @@
// 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;
using osu.Game.Audio;
using osu.Game.Rulesets.Taiko.Objects;
diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
index 046b3a6fd0..10a7495c62 100644
--- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs
@@ -1,10 +1,7 @@
// 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;
-using JetBrains.Annotations;
using osuTK;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@@ -13,6 +10,7 @@ using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.Skinning.Default;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.UI
@@ -29,10 +27,9 @@ namespace osu.Game.Rulesets.Taiko.UI
private double? secondHitTime;
- [CanBeNull]
- public DrawableHitObject JudgedObject;
+ public DrawableHitObject? JudgedObject;
- private SkinnableDrawable skinnable;
+ private SkinnableDrawable skinnable = null!;
///
/// This constructor only exists to meet the new() type constraint of .
@@ -62,7 +59,7 @@ namespace osu.Game.Rulesets.Taiko.UI
skinnable.OnSkinChanged += runAnimation;
}
- public void Apply([CanBeNull] DrawableHitObject drawableHitObject)
+ public void Apply(DrawableHitObject? drawableHitObject)
{
JudgedObject = drawableHitObject;
secondHitTime = null;
diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosionPool.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosionPool.cs
index 8707f7e840..badf34554c 100644
--- a/osu.Game.Rulesets.Taiko/UI/HitExplosionPool.cs
+++ b/osu.Game.Rulesets.Taiko/UI/HitExplosionPool.cs
@@ -1,8 +1,6 @@
// 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 osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Scoring;
diff --git a/osu.Game.Rulesets.Taiko/UI/IAnimatableHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/IAnimatableHitExplosion.cs
index 6a9d43a0ab..cf0f5f9fb6 100644
--- a/osu.Game.Rulesets.Taiko/UI/IAnimatableHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/UI/IAnimatableHitExplosion.cs
@@ -1,8 +1,6 @@
// 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 osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Taiko.UI
diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
index 054f98e18f..6d5b6c5f5d 100644
--- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
+++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs
@@ -1,20 +1,11 @@
// 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;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Sprites;
-using osu.Framework.Graphics.Textures;
-using osu.Framework.Input.Bindings;
-using osu.Framework.Input.Events;
-using osu.Game.Graphics;
-using osu.Game.Screens.Ranking;
+using osu.Game.Rulesets.Taiko.Skinning.Default;
using osu.Game.Skinning;
-using osuTK;
namespace osu.Game.Rulesets.Taiko.UI
{
@@ -23,8 +14,6 @@ namespace osu.Game.Rulesets.Taiko.UI
///
internal class InputDrum : Container
{
- private const float middle_split = 0.025f;
-
public InputDrum()
{
AutoSizeAxes = Axes.X;
@@ -43,166 +32,5 @@ namespace osu.Game.Rulesets.Taiko.UI
},
};
}
-
- private class DefaultInputDrum : AspectContainer
- {
- public DefaultInputDrum()
- {
- RelativeSizeAxes = Axes.Y;
- }
-
- [BackgroundDependencyLoader]
- private void load()
- {
- InternalChild = new Container
- {
- Anchor = Anchor.Centre,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- Scale = new Vector2(0.9f),
- Children = new[]
- {
- new TaikoHalfDrum(false)
- {
- Name = "Left Half",
- Anchor = Anchor.Centre,
- Origin = Anchor.CentreRight,
- RelativeSizeAxes = Axes.Both,
- RelativePositionAxes = Axes.X,
- X = -middle_split / 2,
- RimAction = TaikoAction.LeftRim,
- CentreAction = TaikoAction.LeftCentre
- },
- new TaikoHalfDrum(true)
- {
- Name = "Right Half",
- Anchor = Anchor.Centre,
- Origin = Anchor.CentreLeft,
- RelativeSizeAxes = Axes.Both,
- RelativePositionAxes = Axes.X,
- X = middle_split / 2,
- RimAction = TaikoAction.RightRim,
- CentreAction = TaikoAction.RightCentre
- }
- }
- };
- }
-
- ///
- /// A half-drum. Contains one centre and one rim hit.
- ///
- private class TaikoHalfDrum : Container, IKeyBindingHandler
- {
- ///
- /// The key to be used for the rim of the half-drum.
- ///
- public TaikoAction RimAction;
-
- ///
- /// The key to be used for the centre of the half-drum.
- ///
- public TaikoAction CentreAction;
-
- private readonly Sprite rim;
- private readonly Sprite rimHit;
- private readonly Sprite centre;
- private readonly Sprite centreHit;
-
- public TaikoHalfDrum(bool flipped)
- {
- Masking = true;
-
- Children = new Drawable[]
- {
- rim = new Sprite
- {
- Anchor = flipped ? Anchor.CentreLeft : Anchor.CentreRight,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both
- },
- rimHit = new Sprite
- {
- Anchor = flipped ? Anchor.CentreLeft : Anchor.CentreRight,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- Alpha = 0,
- Blending = BlendingParameters.Additive,
- },
- centre = new Sprite
- {
- Anchor = flipped ? Anchor.CentreLeft : Anchor.CentreRight,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- Size = new Vector2(0.7f)
- },
- centreHit = new Sprite
- {
- Anchor = flipped ? Anchor.CentreLeft : Anchor.CentreRight,
- Origin = Anchor.Centre,
- RelativeSizeAxes = Axes.Both,
- Size = new Vector2(0.7f),
- Alpha = 0,
- Blending = BlendingParameters.Additive
- }
- };
- }
-
- [BackgroundDependencyLoader]
- private void load(TextureStore textures, OsuColour colours)
- {
- rim.Texture = textures.Get(@"Gameplay/taiko/taiko-drum-outer");
- rimHit.Texture = textures.Get(@"Gameplay/taiko/taiko-drum-outer-hit");
- centre.Texture = textures.Get(@"Gameplay/taiko/taiko-drum-inner");
- centreHit.Texture = textures.Get(@"Gameplay/taiko/taiko-drum-inner-hit");
-
- rimHit.Colour = colours.Blue;
- centreHit.Colour = colours.Pink;
- }
-
- public bool OnPressed(KeyBindingPressEvent e)
- {
- Drawable target = null;
- Drawable back = null;
-
- if (e.Action == CentreAction)
- {
- target = centreHit;
- back = centre;
- }
- else if (e.Action == RimAction)
- {
- target = rimHit;
- back = rim;
- }
-
- if (target != null)
- {
- const float scale_amount = 0.05f;
- const float alpha_amount = 0.5f;
-
- const float down_time = 40;
- const float up_time = 1000;
-
- back.ScaleTo(target.Scale.X - scale_amount, down_time, Easing.OutQuint)
- .Then()
- .ScaleTo(1, up_time, Easing.OutQuint);
-
- target.Animate(
- t => t.ScaleTo(target.Scale.X - scale_amount, down_time, Easing.OutQuint),
- t => t.FadeTo(Math.Min(target.Alpha + alpha_amount, 1), down_time, Easing.OutQuint)
- ).Then(
- t => t.ScaleTo(1, up_time, Easing.OutQuint),
- t => t.FadeOut(up_time, Easing.OutQuint)
- );
- }
-
- return false;
- }
-
- public void OnReleased(KeyBindingReleaseEvent e)
- {
- }
- }
- }
}
}
diff --git a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs
index 319d8979ae..c4cff00d2a 100644
--- a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs
+++ b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs
@@ -1,13 +1,12 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.Skinning.Default;
using osu.Game.Skinning;
using osuTK;
@@ -22,7 +21,7 @@ namespace osu.Game.Rulesets.Taiko.UI
private readonly HitType hitType;
- private SkinnableDrawable skinnable;
+ private SkinnableDrawable skinnable = null!;
public override double LifetimeStart => skinnable.Drawable.LifetimeStart;
diff --git a/osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundLeft.cs b/osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundLeft.cs
index db1094e100..2a8890a95d 100644
--- a/osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundLeft.cs
+++ b/osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundLeft.cs
@@ -1,8 +1,6 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
diff --git a/osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundRight.cs b/osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundRight.cs
index 43252e2e77..44bfdacf37 100644
--- a/osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundRight.cs
+++ b/osu.Game.Rulesets.Taiko/UI/PlayfieldBackgroundRight.cs
@@ -1,8 +1,6 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs b/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs
index f48ed2c941..6401c6d09f 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs
@@ -1,8 +1,6 @@
// 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 osuTK;
using osuTK.Graphics;
using osu.Framework.Graphics;
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs
index 26a37fc464..0f214b8436 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs
@@ -1,8 +1,6 @@
// 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;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
@@ -89,7 +87,7 @@ namespace osu.Game.Rulesets.Taiko.UI
[BackgroundDependencyLoader]
private void load(ISkinSource source)
{
- ISkin skin = source.FindProvider(s => getAnimationFrame(s, state, 0) != null);
+ ISkin? skin = source.FindProvider(s => getAnimationFrame(s, state, 0) != null);
if (skin == null) return;
@@ -120,7 +118,7 @@ namespace osu.Game.Rulesets.Taiko.UI
[BackgroundDependencyLoader]
private void load(ISkinSource source)
{
- ISkin skin = source.FindProvider(s => getAnimationFrame(s, TaikoMascotAnimationState.Clear, 0) != null);
+ ISkin? skin = source.FindProvider(s => getAnimationFrame(s, TaikoMascotAnimationState.Clear, 0) != null);
if (skin == null) return;
@@ -137,7 +135,7 @@ namespace osu.Game.Rulesets.Taiko.UI
}
}
- private static Texture getAnimationFrame(ISkin skin, TaikoMascotAnimationState state, int frameIndex)
+ private static Texture? getAnimationFrame(ISkin skin, TaikoMascotAnimationState state, int frameIndex)
{
var texture = skin.GetTexture($"pippidon{state.ToString().ToLowerInvariant()}{frameIndex}");
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimationState.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimationState.cs
index 717f0d725a..02bf245b7b 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimationState.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimationState.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
namespace osu.Game.Rulesets.Taiko.UI
{
public enum TaikoMascotAnimationState
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
index 8e99a82b1b..9cf530e903 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfieldAdjustmentContainer.cs
@@ -1,8 +1,6 @@
// 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;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoReplayRecorder.cs b/osu.Game.Rulesets.Taiko/UI/TaikoReplayRecorder.cs
index a76adc495d..e6391d1386 100644
--- a/osu.Game.Rulesets.Taiko/UI/TaikoReplayRecorder.cs
+++ b/osu.Game.Rulesets.Taiko/UI/TaikoReplayRecorder.cs
@@ -1,8 +1,6 @@
// 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.Collections.Generic;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Taiko.Replays;
diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardDownloadButton.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardDownloadButton.cs
index 10515fd95f..82e18e45bb 100644
--- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardDownloadButton.cs
+++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardDownloadButton.cs
@@ -59,8 +59,9 @@ namespace osu.Game.Tests.Visual.Beatmaps
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
+ Size = new Vector2(25f, 50f),
+ Scale = new Vector2(2f),
State = { Value = DownloadState.NotDownloaded },
- Scale = new Vector2(2)
};
});
}
diff --git a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardFavouriteButton.cs b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardFavouriteButton.cs
index 2fe2264348..9540d9e4f7 100644
--- a/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardFavouriteButton.cs
+++ b/osu.Game.Tests/Visual/Beatmaps/TestSceneBeatmapCardFavouriteButton.cs
@@ -37,7 +37,11 @@ namespace osu.Game.Tests.Visual.Beatmaps
beatmapSetInfo = CreateAPIBeatmapSet(Ruleset.Value);
beatmapSetInfo.HasFavourited = favourited;
});
- AddStep("create button", () => Child = button = new FavouriteButton(beatmapSetInfo) { Scale = new Vector2(2) });
+ AddStep("create button", () => Child = button = new FavouriteButton(beatmapSetInfo)
+ {
+ Size = new Vector2(25f, 50f),
+ Scale = new Vector2(2f),
+ });
assertCorrectIcon(favourited);
AddAssert("correct tooltip text", () => button.TooltipText == (favourited ? BeatmapsetsStrings.ShowDetailsUnfavourite : BeatmapsetsStrings.ShowDetailsFavourite));
@@ -51,7 +55,11 @@ namespace osu.Game.Tests.Visual.Beatmaps
BeatmapFavouriteAction? lastRequestAction = null;
AddStep("create beatmap set", () => beatmapSetInfo = CreateAPIBeatmapSet(Ruleset.Value));
- AddStep("create button", () => Child = button = new FavouriteButton(beatmapSetInfo) { Scale = new Vector2(2) });
+ AddStep("create button", () => Child = button = new FavouriteButton(beatmapSetInfo)
+ {
+ Size = new Vector2(25f, 50f),
+ Scale = new Vector2(2f),
+ });
assertCorrectIcon(false);
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBezierConverter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBezierConverter.cs
new file mode 100644
index 0000000000..28a9d17882
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBezierConverter.cs
@@ -0,0 +1,190 @@
+// 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.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+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 TestSceneBezierConverter : OsuTestScene
+ {
+ private readonly SmoothPath drawablePath;
+ private readonly SmoothPath controlPointDrawablePath;
+ private readonly SmoothPath convertedDrawablePath;
+ private readonly SmoothPath convertedControlPointDrawablePath;
+
+ private SliderPath path = null!;
+ private SliderPath convertedPath = null!;
+
+ public TestSceneBezierConverter()
+ {
+ Children = new Drawable[]
+ {
+ new Container
+ {
+ Children =
+ new Drawable[]
+ {
+ drawablePath = new SmoothPath(),
+ controlPointDrawablePath = new SmoothPath
+ {
+ Colour = Colour4.Magenta,
+ PathRadius = 1f
+ }
+ },
+ Position = new Vector2(100)
+ },
+ new Container
+ {
+ Children =
+ new Drawable[]
+ {
+ convertedDrawablePath = new SmoothPath(),
+ convertedControlPointDrawablePath = new SmoothPath
+ {
+ Colour = Colour4.Magenta,
+ PathRadius = 1f
+ }
+ },
+ Position = new Vector2(100, 300)
+ }
+ };
+
+ resetPath();
+ }
+
+ [SetUp]
+ public void Setup() => Schedule(resetPath);
+
+ private void resetPath()
+ {
+ path = new SliderPath();
+ convertedPath = new SliderPath();
+
+ path.Version.ValueChanged += getConvertedControlPoints;
+ }
+
+ private void getConvertedControlPoints(ValueChangedEvent obj)
+ {
+ convertedPath.ControlPoints.Clear();
+ convertedPath.ControlPoints.AddRange(BezierConverter.ConvertToModernBezier(path.ControlPoints));
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ List vertices = new List();
+
+ path.GetPathToProgress(vertices, 0, 1);
+
+ drawablePath.Vertices = vertices;
+ controlPointDrawablePath.Vertices = path.ControlPoints.Select(o => o.Position).ToList();
+
+ if (controlPointDrawablePath.Vertices.Count > 0)
+ {
+ controlPointDrawablePath.Position =
+ drawablePath.PositionInBoundingBox(drawablePath.Vertices[0]) - controlPointDrawablePath.PositionInBoundingBox(controlPointDrawablePath.Vertices[0]);
+ }
+
+ vertices.Clear();
+
+ convertedPath.GetPathToProgress(vertices, 0, 1);
+
+ convertedDrawablePath.Vertices = vertices;
+ convertedControlPointDrawablePath.Vertices = convertedPath.ControlPoints.Select(o => o.Position).ToList();
+
+ if (convertedControlPointDrawablePath.Vertices.Count > 0)
+ {
+ convertedControlPointDrawablePath.Position = convertedDrawablePath.PositionInBoundingBox(convertedDrawablePath.Vertices[0])
+ - convertedControlPointDrawablePath.PositionInBoundingBox(convertedControlPointDrawablePath.Vertices[0]);
+ }
+ }
+
+ [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 TestComplex()
+ {
+ AddStep("create path", () =>
+ {
+ path.ControlPoints.AddRange(createSegment(PathType.Linear, Vector2.Zero, new Vector2(100, 0)));
+ path.ControlPoints.AddRange(createSegment(PathType.Bezier, new Vector2(100, 0), new Vector2(150, 30), new Vector2(100, 100)));
+ path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, new Vector2(100, 100), new Vector2(25, 50), Vector2.Zero));
+ });
+ }
+
+ [TestCase(0, 100)]
+ [TestCase(1, 100)]
+ [TestCase(5, 100)]
+ [TestCase(10, 100)]
+ [TestCase(30, 100)]
+ [TestCase(50, 100)]
+ [TestCase(100, 100)]
+ [TestCase(100, 1)]
+ public void TestPerfectCurveAngles(float height, float width)
+ {
+ AddStep("create path", () =>
+ {
+ path.ControlPoints.AddRange(createSegment(PathType.PerfectCurve, Vector2.Zero, new Vector2(width / 2, height), new Vector2(width, 0)));
+ });
+ }
+
+ [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;
+ }
+ });
+ }
+
+ private List createSegment(PathType type, params Vector2[] controlPoints)
+ {
+ var points = controlPoints.Select(p => new PathControlPoint { Position = p }).ToList();
+ points[0].Type = type;
+ return points;
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs
index 6b02449aa3..1bba62a5cf 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs
@@ -93,6 +93,15 @@ namespace osu.Game.Tests.Visual.Gameplay
checkRequestCount(1);
}
+ [Test]
+ public void TestAutomaticSkipActuatesOnce()
+ {
+ createTest();
+ AddStep("start automated skip", () => skip.SkipWhenReady());
+ AddUntilStep("wait for button disabled", () => !skip.IsButtonVisible);
+ checkRequestCount(1);
+ }
+
[Test]
public void TestClickOnlyActuatesOnce()
{
@@ -110,6 +119,16 @@ namespace osu.Game.Tests.Visual.Gameplay
checkRequestCount(1);
}
+ [Test]
+ public void TestAutomaticSkipActuatesMultipleTimes()
+ {
+ createTest();
+ AddStep("set increment lower", () => increment = 3000);
+ AddStep("start automated skip", () => skip.SkipWhenReady());
+ AddUntilStep("wait for button disabled", () => !skip.IsButtonVisible);
+ checkRequestCount(2);
+ }
+
[Test]
public void TestClickOnlyActuatesMultipleTimes()
{
@@ -137,8 +156,11 @@ namespace osu.Game.Tests.Visual.Gameplay
checkRequestCount(0);
}
- private void checkRequestCount(int expected) =>
- AddAssert($"request count is {expected}", () => requestCount == expected);
+ private void checkRequestCount(int expected)
+ {
+ AddAssert($"skip count is {expected}", () => skip.SkipCount, () => Is.EqualTo(expected));
+ AddAssert($"request count is {expected}", () => requestCount, () => Is.EqualTo(expected));
+ }
private class TestSkipOverlay : SkipOverlay
{
diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs
index bf01d3b0ac..b4ffcd42b5 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneCommentActions.cs
@@ -10,10 +10,12 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
using osu.Framework.Testing;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
@@ -37,14 +39,20 @@ namespace osu.Game.Tests.Visual.Online
private CommentsContainer commentsContainer = null!;
+ private readonly ManualResetEventSlim requestLock = new ManualResetEventSlim();
+
[BackgroundDependencyLoader]
private void load()
{
base.Content.AddRange(new Drawable[]
{
- content = new OsuScrollContainer
+ new PopoverContainer
{
- RelativeSizeAxes = Axes.Both
+ RelativeSizeAxes = Axes.Both,
+ Child = content = new OsuScrollContainer
+ {
+ RelativeSizeAxes = Axes.Both
+ }
},
dialogOverlay
});
@@ -80,8 +88,6 @@ namespace osu.Game.Tests.Visual.Online
});
}
- private readonly ManualResetEventSlim deletionPerformed = new ManualResetEventSlim();
-
[Test]
public void TestDeletion()
{
@@ -105,7 +111,7 @@ namespace osu.Game.Tests.Visual.Online
});
AddStep("Setup request handling", () =>
{
- deletionPerformed.Reset();
+ requestLock.Reset();
dummyAPI.HandleRequest = request =>
{
@@ -138,7 +144,7 @@ namespace osu.Game.Tests.Visual.Online
Task.Run(() =>
{
- deletionPerformed.Wait(10000);
+ requestLock.Wait(10000);
req.TriggerSuccess(cb);
});
@@ -149,7 +155,7 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("Loading spinner shown", () => commentsContainer.ChildrenOfType().Any(d => d.IsPresent));
- AddStep("Complete request", () => deletionPerformed.Set());
+ AddStep("Complete request", () => requestLock.Set());
AddUntilStep("Comment is deleted locally", () => this.ChildrenOfType().Single(x => x.Comment.Id == 1).WasDeleted);
}
@@ -204,6 +210,74 @@ namespace osu.Game.Tests.Visual.Online
});
}
+ [Test]
+ public void TestReport()
+ {
+ const string report_text = "I don't like this comment";
+ DrawableComment? targetComment = null;
+ CommentReportRequest? request = null;
+
+ addTestComments();
+ AddUntilStep("Comment exists", () =>
+ {
+ var comments = this.ChildrenOfType();
+ targetComment = comments.SingleOrDefault(x => x.Comment.Id == 2);
+ return targetComment != null;
+ });
+ AddStep("Setup request handling", () =>
+ {
+ requestLock.Reset();
+
+ dummyAPI.HandleRequest = r =>
+ {
+ if (!(r is CommentReportRequest req))
+ return false;
+
+ Task.Run(() =>
+ {
+ request = req;
+ requestLock.Wait(10000);
+ req.TriggerSuccess();
+ });
+
+ return true;
+ };
+ });
+ AddStep("Click the button", () =>
+ {
+ var btn = targetComment.ChildrenOfType().Single(x => x.Text == "Report");
+ InputManager.MoveMouseTo(btn);
+ InputManager.Click(MouseButton.Left);
+ });
+ AddStep("Try to report", () =>
+ {
+ var btn = this.ChildrenOfType().Single().ChildrenOfType().Single();
+ InputManager.MoveMouseTo(btn);
+ InputManager.Click(MouseButton.Left);
+ });
+ AddWaitStep("Wait", 3);
+ AddAssert("Nothing happened", () => this.ChildrenOfType().Any());
+ AddStep("Set report data", () =>
+ {
+ var field = this.ChildrenOfType().Single();
+ field.Current.Value = report_text;
+ var reason = this.ChildrenOfType>().Single();
+ reason.Current.Value = CommentReportReason.Other;
+ });
+ AddStep("Try to report", () =>
+ {
+ var btn = this.ChildrenOfType().Single().ChildrenOfType().Single();
+ InputManager.MoveMouseTo(btn);
+ InputManager.Click(MouseButton.Left);
+ });
+ AddWaitStep("Wait", 3);
+ AddAssert("Overlay closed", () => !this.ChildrenOfType().Any());
+ AddAssert("Loading spinner shown", () => targetComment.ChildrenOfType().Any(d => d.IsPresent));
+ AddStep("Complete request", () => requestLock.Set());
+ AddUntilStep("Request sent", () => request != null);
+ AddAssert("Request is correct", () => request != null && request.CommentID == 2 && request.Comment == report_text && request.Reason == CommentReportReason.Other);
+ }
+
private void addTestComments()
{
AddStep("set up response", () =>
diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentReportButton.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentReportButton.cs
new file mode 100644
index 0000000000..fb56a41507
--- /dev/null
+++ b/osu.Game.Tests/Visual/Online/TestSceneCommentReportButton.cs
@@ -0,0 +1,46 @@
+// 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.Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Testing;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Overlays.Comments;
+using osu.Game.Tests.Visual.UserInterface;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.Online
+{
+ public class TestSceneCommentReportButton : ThemeComparisonTestScene
+ {
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("setup API", () => ((DummyAPIAccess)API).HandleRequest += req =>
+ {
+ switch (req)
+ {
+ case CommentReportRequest report:
+ Scheduler.AddDelayed(report.TriggerSuccess, 1000);
+ return true;
+ }
+
+ return false;
+ });
+ }
+
+ protected override Drawable CreateContent() => new PopoverContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = new CommentReportButton(new Comment { User = new APIUser { Username = "Someone" } })
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Scale = new Vector2(2f),
+ }.With(b => Schedule(b.ShowPopover)),
+ };
+ }
+}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
index caa2d2571d..7064a08151 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs
@@ -52,8 +52,15 @@ namespace osu.Game.Tests.Visual.Online
{
AwardedAt = DateTimeOffset.FromUnixTimeSeconds(1505741569),
Description = "Outstanding help by being a voluntary test subject.",
- ImageUrl = "https://assets.ppy.sh/profile-badges/contributor.jpg"
- }
+ ImageUrl = "https://assets.ppy.sh/profile-badges/contributor.jpg",
+ Url = "https://osu.ppy.sh/wiki/en/People/Community_Contributors",
+ },
+ new Badge
+ {
+ AwardedAt = DateTimeOffset.FromUnixTimeSeconds(1505741569),
+ Description = "Badge without a url.",
+ ImageUrl = "https://assets.ppy.sh/profile-badges/contributor.jpg",
+ },
},
Title = "osu!volunteer",
Colour = "ff0000",
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index cc8746959b..63532fdba8 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
@@ -32,7 +31,6 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
-using osu.Game.Rulesets.Taiko;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Select;
@@ -538,36 +536,6 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("selection shown on wedge", () => songSelect!.CurrentBeatmapDetailsBeatmap.BeatmapInfo.MatchesOnlineID(target));
}
- [Test]
- public void TestRulesetChangeResetsMods()
- {
- createSongSelect();
- changeRuleset(0);
-
- changeMods(new OsuModHardRock());
-
- int actionIndex = 0;
- int modChangeIndex = 0;
- int rulesetChangeIndex = 0;
-
- AddStep("change ruleset", () =>
- {
- SelectedMods.ValueChanged += onModChange;
- songSelect!.Ruleset.ValueChanged += onRulesetChange;
-
- Ruleset.Value = new TaikoRuleset().RulesetInfo;
-
- SelectedMods.ValueChanged -= onModChange;
- songSelect!.Ruleset.ValueChanged -= onRulesetChange;
- });
-
- AddAssert("mods changed before ruleset", () => modChangeIndex < rulesetChangeIndex);
- AddAssert("empty mods", () => !SelectedMods.Value.Any());
-
- void onModChange(ValueChangedEvent> e) => modChangeIndex = actionIndex++;
- void onRulesetChange(ValueChangedEvent e) => rulesetChangeIndex = actionIndex++;
- }
-
[Test]
public void TestModsRetainedBetweenSongSelect()
{
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs
index e5f3aea2f7..5548375af2 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs
@@ -38,6 +38,14 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("revert back", () => this.ChildrenOfType>().ForEach(l => l.ResizeWidthTo(1, 200, Easing.OutQuint)));
}
+ [Test]
+ public void TestDisable()
+ {
+ createSliderBar();
+ AddStep("set disabled", () => this.ChildrenOfType>().ForEach(l => l.Current.Disabled = true));
+ AddStep("unset disabled", () => this.ChildrenOfType>().ForEach(l => l.Current.Disabled = false));
+ }
+
private void createSliderBar()
{
AddStep("create component", () =>
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
index 4c43a2fdcd..0292ce5905 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs
@@ -17,6 +17,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets;
+using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
@@ -338,26 +339,36 @@ namespace osu.Game.Tests.Visual.UserInterface
}
[Test]
- public void TestRulesetChanges()
+ public void TestCommonModsMaintainedOnRulesetChange()
{
createScreen();
changeRuleset(0);
- var noFailMod = new OsuRuleset().GetModsFor(ModType.DifficultyReduction).FirstOrDefault(m => m is OsuModNoFail);
-
- AddStep("set mods externally", () => { SelectedMods.Value = new[] { noFailMod }; });
+ AddStep("select relax mod", () => SelectedMods.Value = new[] { Ruleset.Value.CreateInstance().CreateMod() });
changeRuleset(0);
+ AddAssert("ensure mod still selected", () => SelectedMods.Value.SingleOrDefault() is OsuModRelax);
- AddAssert("ensure mods still selected", () => SelectedMods.Value.SingleOrDefault(m => m is OsuModNoFail) != null);
+ changeRuleset(2);
+ AddAssert("catch variant selected", () => SelectedMods.Value.SingleOrDefault() is CatchModRelax);
changeRuleset(3);
+ AddAssert("no mod selected", () => SelectedMods.Value.Count == 0);
+ }
- AddAssert("ensure mods not selected", () => SelectedMods.Value.Count == 0);
-
+ [Test]
+ public void TestUncommonModsDiscardedOnRulesetChange()
+ {
+ createScreen();
changeRuleset(0);
- AddAssert("ensure mods not selected", () => SelectedMods.Value.Count == 0);
+ AddStep("select single tap mod", () => SelectedMods.Value = new[] { new OsuModSingleTap() });
+
+ changeRuleset(0);
+ AddAssert("ensure mod still selected", () => SelectedMods.Value.SingleOrDefault() is OsuModSingleTap);
+
+ changeRuleset(3);
+ AddAssert("no mod selected", () => SelectedMods.Value.Count == 0);
}
[Test]
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index bdf8cc5136..24969414d0 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -1,11 +1,11 @@
-
+
-
+
diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
index bdef46a6b2..9f2a088a4b 100644
--- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
+++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj
@@ -6,7 +6,7 @@
-
+
WinExe
diff --git a/osu.Game.Tournament/Models/SeedingBeatmap.cs b/osu.Game.Tournament/Models/SeedingBeatmap.cs
index fb0e20556c..0ac312342c 100644
--- a/osu.Game.Tournament/Models/SeedingBeatmap.cs
+++ b/osu.Game.Tournament/Models/SeedingBeatmap.cs
@@ -18,7 +18,7 @@ namespace osu.Game.Tournament.Models
public Bindable Seed = new BindableInt
{
MinValue = 1,
- MaxValue = 64
+ MaxValue = 256
};
}
}
diff --git a/osu.Game.Tournament/Models/SeedingResult.cs b/osu.Game.Tournament/Models/SeedingResult.cs
index 71e52b3324..2a404153e6 100644
--- a/osu.Game.Tournament/Models/SeedingResult.cs
+++ b/osu.Game.Tournament/Models/SeedingResult.cs
@@ -17,7 +17,7 @@ namespace osu.Game.Tournament.Models
public Bindable Seed = new BindableInt
{
MinValue = 1,
- MaxValue = 64
+ MaxValue = 256
};
}
}
diff --git a/osu.Game.Tournament/Models/TournamentTeam.cs b/osu.Game.Tournament/Models/TournamentTeam.cs
index ac57f748da..1beea517d5 100644
--- a/osu.Game.Tournament/Models/TournamentTeam.cs
+++ b/osu.Game.Tournament/Models/TournamentTeam.cs
@@ -54,7 +54,7 @@ namespace osu.Game.Tournament.Models
public Bindable LastYearPlacing = new BindableInt
{
MinValue = 1,
- MaxValue = 64
+ MaxValue = 256
};
[JsonProperty]
diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs
index 4b9e5d9ae4..646c990564 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardExtra.cs
@@ -81,7 +81,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards
FavouriteState = { BindTarget = FavouriteState },
ButtonsCollapsedWidth = CORNER_RADIUS,
ButtonsExpandedWidth = 30,
- ButtonsPadding = new MarginPadding { Vertical = 35 },
Children = new Drawable[]
{
new FillFlowContainer
diff --git a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs
index d9ce64879f..addc88700c 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/BeatmapCardNormal.cs
@@ -82,7 +82,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards
FavouriteState = { BindTarget = FavouriteState },
ButtonsCollapsedWidth = CORNER_RADIUS,
ButtonsExpandedWidth = 30,
- ButtonsPadding = new MarginPadding { Vertical = 17.5f },
Children = new Drawable[]
{
new FillFlowContainer
diff --git a/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs b/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs
index c5b251cc2b..af1a8eb06a 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/Buttons/BeatmapCardIconButton.cs
@@ -4,13 +4,16 @@
#nullable disable
using osu.Framework.Allocation;
+using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
using osuTK;
+using osuTK.Graphics;
namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
{
@@ -59,6 +62,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
protected override Container Content => content;
private readonly Container content;
+ private readonly Box hover;
protected BeatmapCardIconButton()
{
@@ -69,19 +73,27 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
{
RelativeSizeAxes = Axes.Both,
Masking = true,
+ CornerRadius = BeatmapCard.CORNER_RADIUS,
+ Scale = new Vector2(0.8f),
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Children = new Drawable[]
{
+ hover = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.White.Opacity(0.1f),
+ Blending = BlendingParameters.Additive,
+ },
Icon = new SpriteIcon
{
Origin = Anchor.Centre,
- Anchor = Anchor.Centre
- }
+ Anchor = Anchor.Centre,
+ Scale = new Vector2(1.2f),
+ },
}
});
- Size = new Vector2(24);
IconSize = 12;
}
@@ -116,8 +128,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
{
bool isHovered = IsHovered && Enabled.Value;
- content.ScaleTo(isHovered ? 1.2f : 1, 500, Easing.OutQuint);
- content.FadeColour(isHovered ? HoverColour : IdleColour, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
+ hover.FadeTo(isHovered ? 1f : 0f, 500, Easing.OutQuint);
+ content.ScaleTo(isHovered ? 1 : 0.8f, 500, Easing.OutQuint);
+ Icon.FadeColour(isHovered ? HoverColour : IdleColour, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
}
}
}
diff --git a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs
index 107c126eb5..9b200d62aa 100644
--- a/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs
+++ b/osu.Game/Beatmaps/Drawables/Cards/CollapsibleButtonContainer.cs
@@ -48,12 +48,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards
}
}
- public MarginPadding ButtonsPadding
- {
- get => buttons.Padding;
- set => buttons.Padding = value;
- }
-
protected override Container Content => mainContent;
private readonly Container background;
@@ -86,9 +80,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
- // workaround for masking artifacts at the top & bottom of card,
- // which become especially visible on downloaded beatmaps (when the icon area has a lime background).
- Padding = new MarginPadding { Vertical = 1 },
Child = new Box
{
RelativeSizeAxes = Axes.Both,
@@ -104,25 +95,34 @@ namespace osu.Game.Beatmaps.Drawables.Cards
Child = buttons = new Container
{
RelativeSizeAxes = Axes.Both,
+ // Padding of 4 avoids touching the card borders when in the expanded (ie. showing difficulties) state.
+ // Left override allows the buttons to visually be wider and look better.
+ Padding = new MarginPadding(4) { Left = 2 },
Children = new BeatmapCardIconButton[]
{
new FavouriteButton(beatmapSet)
{
Current = FavouriteState,
Anchor = Anchor.TopCentre,
- Origin = Anchor.TopCentre
+ Origin = Anchor.TopCentre,
+ RelativeSizeAxes = Axes.Both,
+ Height = 0.48f,
},
new DownloadButton(beatmapSet)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
- State = { BindTarget = downloadTracker.State }
+ State = { BindTarget = downloadTracker.State },
+ RelativeSizeAxes = Axes.Both,
+ Height = 0.48f,
},
new GoToBeatmapButton(beatmapSet)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
- State = { BindTarget = downloadTracker.State }
+ State = { BindTarget = downloadTracker.State },
+ RelativeSizeAxes = Axes.Both,
+ Height = 0.48f,
}
}
}
diff --git a/osu.Game/Graphics/Containers/OsuClickableContainer.cs b/osu.Game/Graphics/Containers/OsuClickableContainer.cs
index 61ec9dfc24..03a1cfcc13 100644
--- a/osu.Game/Graphics/Containers/OsuClickableContainer.cs
+++ b/osu.Game/Graphics/Containers/OsuClickableContainer.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Graphics.Containers
protected override Container Content => content;
- protected virtual HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverClickSounds(sampleSet);
+ protected virtual HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverClickSounds(sampleSet) { Enabled = { BindTarget = Enabled } };
public OsuClickableContainer(HoverSampleSet sampleSet = HoverSampleSet.Default)
{
diff --git a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs
index b469c21ab0..6c8eeed391 100644
--- a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs
+++ b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs
@@ -24,6 +24,7 @@ namespace osu.Game.Graphics.UserInterface
private const int transition_length = 80;
private TextContainer text;
+ private HoverClickSounds hoverClickSounds;
public DrawableOsuMenuItem(MenuItem item)
: base(item)
@@ -36,7 +37,7 @@ namespace osu.Game.Graphics.UserInterface
BackgroundColour = Color4.Transparent;
BackgroundColourHover = Color4Extensions.FromHex(@"172023");
- AddInternal(new HoverClickSounds());
+ AddInternal(hoverClickSounds = new HoverClickSounds());
updateTextColour();
@@ -76,6 +77,7 @@ namespace osu.Game.Graphics.UserInterface
private void updateState()
{
+ hoverClickSounds.Enabled.Value = !Item.Action.Disabled;
Alpha = Item.Action.Disabled ? 0.2f : 1;
if (IsHovered && !Item.Action.Disabled)
diff --git a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs
index dab4390ede..89d1570cd4 100644
--- a/osu.Game/Graphics/UserInterface/HoverClickSounds.cs
+++ b/osu.Game/Graphics/UserInterface/HoverClickSounds.cs
@@ -7,6 +7,7 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
+using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
@@ -20,7 +21,11 @@ namespace osu.Game.Graphics.UserInterface
///
public class HoverClickSounds : HoverSounds
{
+ public Bindable Enabled = new Bindable(true);
+
private Sample sampleClick;
+ private Sample sampleClickDisabled;
+
private readonly MouseButton[] buttons;
///
@@ -41,8 +46,13 @@ namespace osu.Game.Graphics.UserInterface
{
if (buttons.Contains(e.Button) && Contains(e.ScreenSpaceMousePosition))
{
- sampleClick.Frequency.Value = 0.99 + RNG.NextDouble(0.02);
- sampleClick.Play();
+ var channel = Enabled.Value ? sampleClick?.GetChannel() : sampleClickDisabled?.GetChannel();
+
+ if (channel != null)
+ {
+ channel.Frequency.Value = 0.99 + RNG.NextDouble(0.02);
+ channel.Play();
+ }
}
return base.OnClick(e);
@@ -53,6 +63,9 @@ namespace osu.Game.Graphics.UserInterface
{
sampleClick = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-select")
?? audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select");
+
+ sampleClickDisabled = audio.Samples.Get($@"UI/{SampleSet.GetDescription()}-select-disabled")
+ ?? audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select-disabled");
}
}
}
diff --git a/osu.Game/Graphics/UserInterface/LoadingButton.cs b/osu.Game/Graphics/UserInterface/LoadingButton.cs
index 8be50a4b43..44067bac8b 100644
--- a/osu.Game/Graphics/UserInterface/LoadingButton.cs
+++ b/osu.Game/Graphics/UserInterface/LoadingButton.cs
@@ -41,6 +41,7 @@ namespace osu.Game.Graphics.UserInterface
private readonly LoadingSpinner loading;
protected LoadingButton()
+ : base(HoverSampleSet.Button)
{
Add(loading = new LoadingSpinner
{
diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs
index 291ff644fd..88f3ccd191 100644
--- a/osu.Game/Graphics/UserInterface/OsuButton.cs
+++ b/osu.Game/Graphics/UserInterface/OsuButton.cs
@@ -104,7 +104,7 @@ namespace osu.Game.Graphics.UserInterface
});
if (hoverSounds.HasValue)
- AddInternal(new HoverClickSounds(hoverSounds.Value));
+ AddInternal(new HoverClickSounds(hoverSounds.Value) { Enabled = { BindTarget = Enabled } });
}
[BackgroundDependencyLoader]
diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
index 9acb0c7f94..392740690a 100644
--- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
+++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
@@ -46,6 +46,8 @@ namespace osu.Game.Graphics.UserInterface
public bool PlaySamplesOnAdjust { get; set; } = true;
+ private readonly HoverClickSounds hoverClickSounds;
+
///
/// Whether to format the tooltip as a percentage or the actual value.
///
@@ -127,7 +129,7 @@ namespace osu.Game.Graphics.UserInterface
Current = { Value = true }
},
},
- new HoverClickSounds()
+ hoverClickSounds = new HoverClickSounds()
};
Current.DisabledChanged += disabled => { Alpha = disabled ? 0.3f : 1; };
@@ -152,6 +154,7 @@ namespace osu.Game.Graphics.UserInterface
{
base.LoadComplete();
CurrentNumber.BindValueChanged(current => TooltipText = getTooltipText(current.NewValue), true);
+ Current.DisabledChanged += disabled => hoverClickSounds.Enabled.Value = !disabled;
}
protected override bool OnHover(HoverEvent e)
diff --git a/osu.Game/Graphics/UserInterface/ShearedButton.cs b/osu.Game/Graphics/UserInterface/ShearedButton.cs
index 0c25d06cd4..e406e273e6 100644
--- a/osu.Game/Graphics/UserInterface/ShearedButton.cs
+++ b/osu.Game/Graphics/UserInterface/ShearedButton.cs
@@ -138,7 +138,7 @@ namespace osu.Game.Graphics.UserInterface
}
}
- protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverClickSounds(sampleSet);
+ protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverClickSounds(sampleSet) { Enabled = { BindTarget = Enabled } };
protected override void LoadComplete()
{
diff --git a/osu.Game/Online/API/Requests/CommentReportRequest.cs b/osu.Game/Online/API/Requests/CommentReportRequest.cs
new file mode 100644
index 0000000000..3f57756ced
--- /dev/null
+++ b/osu.Game/Online/API/Requests/CommentReportRequest.cs
@@ -0,0 +1,38 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Net.Http;
+using osu.Framework.IO.Network;
+using osu.Game.Overlays.Comments;
+
+namespace osu.Game.Online.API.Requests
+{
+ public class CommentReportRequest : APIRequest
+ {
+ public readonly long CommentID;
+ public readonly CommentReportReason Reason;
+ public readonly string Comment;
+
+ public CommentReportRequest(long commentID, CommentReportReason reason, string comment)
+ {
+ CommentID = commentID;
+ Reason = reason;
+ Comment = comment;
+ }
+
+ protected override WebRequest CreateWebRequest()
+ {
+ var req = base.CreateWebRequest();
+ req.Method = HttpMethod.Post;
+
+ req.AddParameter(@"reportable_type", @"comment");
+ req.AddParameter(@"reportable_id", $"{CommentID}");
+ req.AddParameter(@"reason", Reason.ToString());
+ req.AddParameter(@"comments", Comment);
+
+ return req;
+ }
+
+ protected override string Target => @"reports";
+ }
+}
diff --git a/osu.Game/Online/HubClient.cs b/osu.Game/Online/HubClient.cs
new file mode 100644
index 0000000000..583f15a4a4
--- /dev/null
+++ b/osu.Game/Online/HubClient.cs
@@ -0,0 +1,28 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.SignalR.Client;
+
+namespace osu.Game.Online
+{
+ public class HubClient : PersistentEndpointClient
+ {
+ public readonly HubConnection Connection;
+
+ public HubClient(HubConnection connection)
+ {
+ Connection = connection;
+ Connection.Closed += InvokeClosed;
+ }
+
+ public override Task ConnectAsync(CancellationToken cancellationToken) => Connection.StartAsync(cancellationToken);
+
+ public override async ValueTask DisposeAsync()
+ {
+ await base.DisposeAsync().ConfigureAwait(false);
+ await Connection.DisposeAsync().ConfigureAwait(false);
+ }
+ }
+}
diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs
index 6bfe09e911..6f246f6dd3 100644
--- a/osu.Game/Online/HubClientConnector.cs
+++ b/osu.Game/Online/HubClientConnector.cs
@@ -10,13 +10,11 @@ using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using osu.Framework;
-using osu.Framework.Bindables;
-using osu.Framework.Logging;
using osu.Game.Online.API;
namespace osu.Game.Online
{
- public class HubClientConnector : IHubClientConnector
+ public class HubClientConnector : PersistentEndpointClientConnector, IHubClientConnector
{
public const string SERVER_SHUTDOWN_MESSAGE = "Server is shutting down.";
@@ -25,7 +23,6 @@ namespace osu.Game.Online
///
public Action? ConfigureConnection { get; set; }
- private readonly string clientName;
private readonly string endpoint;
private readonly string versionHash;
private readonly bool preferMessagePack;
@@ -34,18 +31,7 @@ namespace osu.Game.Online
///
/// The current connection opened by this connector.
///
- public HubConnection? CurrentConnection { get; private set; }
-
- ///
- /// Whether this is connected to the hub, use to access the connection, if this is true.
- ///
- public IBindable IsConnected => isConnected;
-
- private readonly Bindable isConnected = new Bindable();
- private readonly SemaphoreSlim connectionLock = new SemaphoreSlim(1);
- private CancellationTokenSource connectCancelSource = new CancellationTokenSource();
-
- private readonly IBindable apiState = new Bindable();
+ public new HubConnection? CurrentConnection => ((HubClient?)base.CurrentConnection)?.Connection;
///
/// Constructs a new .
@@ -56,99 +42,16 @@ namespace osu.Game.Online
/// The hash representing the current game version, used for verification purposes.
/// Whether to use MessagePack for serialisation if available on this platform.
public HubClientConnector(string clientName, string endpoint, IAPIProvider api, string versionHash, bool preferMessagePack = true)
+ : base(api)
{
- this.clientName = clientName;
+ ClientName = clientName;
this.endpoint = endpoint;
this.api = api;
this.versionHash = versionHash;
this.preferMessagePack = preferMessagePack;
-
- apiState.BindTo(api.State);
- apiState.BindValueChanged(_ => Task.Run(connectIfPossible), true);
}
- public Task Reconnect()
- {
- Logger.Log($"{clientName} reconnecting...", LoggingTarget.Network);
- return Task.Run(connectIfPossible);
- }
-
- private async Task connectIfPossible()
- {
- switch (apiState.Value)
- {
- case APIState.Failing:
- case APIState.Offline:
- await disconnect(true);
- break;
-
- case APIState.Online:
- await connect();
- break;
- }
- }
-
- private async Task connect()
- {
- cancelExistingConnect();
-
- if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false))
- throw new TimeoutException("Could not obtain a lock to connect. A previous attempt is likely stuck.");
-
- try
- {
- while (apiState.Value == APIState.Online)
- {
- // ensure any previous connection was disposed.
- // this will also create a new cancellation token source.
- await disconnect(false).ConfigureAwait(false);
-
- // this token will be valid for the scope of this connection.
- // if cancelled, we can be sure that a disconnect or reconnect is handled elsewhere.
- var cancellationToken = connectCancelSource.Token;
-
- cancellationToken.ThrowIfCancellationRequested();
-
- Logger.Log($"{clientName} connecting...", LoggingTarget.Network);
-
- try
- {
- // importantly, rebuild the connection each attempt to get an updated access token.
- CurrentConnection = buildConnection(cancellationToken);
-
- await CurrentConnection.StartAsync(cancellationToken).ConfigureAwait(false);
-
- Logger.Log($"{clientName} connected!", LoggingTarget.Network);
- isConnected.Value = true;
- return;
- }
- catch (OperationCanceledException)
- {
- //connection process was cancelled.
- throw;
- }
- catch (Exception e)
- {
- await handleErrorAndDelay(e, cancellationToken).ConfigureAwait(false);
- }
- }
- }
- finally
- {
- connectionLock.Release();
- }
- }
-
- ///
- /// Handles an exception and delays an async flow.
- ///
- private async Task handleErrorAndDelay(Exception exception, CancellationToken cancellationToken)
- {
- Logger.Log($"{clientName} connect attempt failed: {exception.Message}", LoggingTarget.Network);
- await Task.Delay(5000, cancellationToken).ConfigureAwait(false);
- }
-
- private HubConnection buildConnection(CancellationToken cancellationToken)
+ protected override Task BuildConnectionAsync(CancellationToken cancellationToken)
{
var builder = new HubConnectionBuilder()
.WithUrl(endpoint, options =>
@@ -188,59 +91,9 @@ namespace osu.Game.Online
ConfigureConnection?.Invoke(newConnection);
- newConnection.Closed += ex => onConnectionClosed(ex, cancellationToken);
- return newConnection;
+ return Task.FromResult((PersistentEndpointClient)new HubClient(newConnection));
}
- private async Task onConnectionClosed(Exception? ex, CancellationToken cancellationToken)
- {
- isConnected.Value = false;
-
- if (ex != null)
- await handleErrorAndDelay(ex, cancellationToken).ConfigureAwait(false);
- else
- Logger.Log($"{clientName} disconnected", LoggingTarget.Network);
-
- // make sure a disconnect wasn't triggered (and this is still the active connection).
- if (!cancellationToken.IsCancellationRequested)
- await Task.Run(connect, default).ConfigureAwait(false);
- }
-
- private async Task disconnect(bool takeLock)
- {
- cancelExistingConnect();
-
- if (takeLock)
- {
- if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false))
- throw new TimeoutException("Could not obtain a lock to disconnect. A previous attempt is likely stuck.");
- }
-
- try
- {
- if (CurrentConnection != null)
- await CurrentConnection.DisposeAsync().ConfigureAwait(false);
- }
- finally
- {
- CurrentConnection = null;
- if (takeLock)
- connectionLock.Release();
- }
- }
-
- private void cancelExistingConnect()
- {
- connectCancelSource.Cancel();
- connectCancelSource = new CancellationTokenSource();
- }
-
- public override string ToString() => $"Connector for {clientName} ({(IsConnected.Value ? "connected" : "not connected")}";
-
- public void Dispose()
- {
- apiState.UnbindAll();
- cancelExistingConnect();
- }
+ protected override string ClientName { get; }
}
}
diff --git a/osu.Game/Online/PersistentEndpointClient.cs b/osu.Game/Online/PersistentEndpointClient.cs
new file mode 100644
index 0000000000..32c243fbbb
--- /dev/null
+++ b/osu.Game/Online/PersistentEndpointClient.cs
@@ -0,0 +1,35 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace osu.Game.Online
+{
+ public abstract class PersistentEndpointClient : IAsyncDisposable
+ {
+ ///
+ /// An event notifying the that the connection has been closed
+ ///
+ public event Func? Closed;
+
+ ///
+ /// Notifies the that the connection has been closed.
+ ///
+ /// The exception that the connection closed with.
+ protected Task InvokeClosed(Exception? exception) => Closed?.Invoke(exception) ?? Task.CompletedTask;
+
+ ///
+ /// Connects the client to the remote service to begin processing messages.
+ ///
+ /// A cancellation token to stop processing messages.
+ public abstract Task ConnectAsync(CancellationToken cancellationToken);
+
+ public virtual ValueTask DisposeAsync()
+ {
+ Closed = null;
+ return new ValueTask(Task.CompletedTask);
+ }
+ }
+}
diff --git a/osu.Game/Online/PersistentEndpointClientConnector.cs b/osu.Game/Online/PersistentEndpointClientConnector.cs
new file mode 100644
index 0000000000..70e10c6c7d
--- /dev/null
+++ b/osu.Game/Online/PersistentEndpointClientConnector.cs
@@ -0,0 +1,198 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using osu.Framework.Bindables;
+using osu.Framework.Extensions.TypeExtensions;
+using osu.Framework.Logging;
+using osu.Game.Online.API;
+
+namespace osu.Game.Online
+{
+ public abstract class PersistentEndpointClientConnector : IDisposable
+ {
+ ///
+ /// Whether the managed connection is currently connected. When true use to access the connection.
+ ///
+ public IBindable IsConnected => isConnected;
+
+ ///
+ /// The current connection opened by this connector.
+ ///
+ public PersistentEndpointClient? CurrentConnection { get; private set; }
+
+ private readonly Bindable isConnected = new Bindable();
+ private readonly SemaphoreSlim connectionLock = new SemaphoreSlim(1);
+ private CancellationTokenSource connectCancelSource = new CancellationTokenSource();
+
+ private readonly IBindable apiState = new Bindable();
+
+ ///
+ /// Constructs a new .
+ ///
+ /// An API provider used to react to connection state changes.
+ protected PersistentEndpointClientConnector(IAPIProvider api)
+ {
+ apiState.BindTo(api.State);
+ apiState.BindValueChanged(_ => Task.Run(connectIfPossible), true);
+ }
+
+ public Task Reconnect()
+ {
+ Logger.Log($"{ClientName} reconnecting...", LoggingTarget.Network);
+ return Task.Run(connectIfPossible);
+ }
+
+ private async Task connectIfPossible()
+ {
+ switch (apiState.Value)
+ {
+ case APIState.Failing:
+ case APIState.Offline:
+ await disconnect(true);
+ break;
+
+ case APIState.Online:
+ await connect();
+ break;
+ }
+ }
+
+ private async Task connect()
+ {
+ cancelExistingConnect();
+
+ if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false))
+ throw new TimeoutException("Could not obtain a lock to connect. A previous attempt is likely stuck.");
+
+ try
+ {
+ while (apiState.Value == APIState.Online)
+ {
+ // ensure any previous connection was disposed.
+ // this will also create a new cancellation token source.
+ await disconnect(false).ConfigureAwait(false);
+
+ // this token will be valid for the scope of this connection.
+ // if cancelled, we can be sure that a disconnect or reconnect is handled elsewhere.
+ var cancellationToken = connectCancelSource.Token;
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ Logger.Log($"{ClientName} connecting...", LoggingTarget.Network);
+
+ try
+ {
+ // importantly, rebuild the connection each attempt to get an updated access token.
+ CurrentConnection = await BuildConnectionAsync(cancellationToken).ConfigureAwait(false);
+ CurrentConnection.Closed += ex => onConnectionClosed(ex, cancellationToken);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ await CurrentConnection.ConnectAsync(cancellationToken).ConfigureAwait(false);
+
+ Logger.Log($"{ClientName} connected!", LoggingTarget.Network);
+ isConnected.Value = true;
+ return;
+ }
+ catch (OperationCanceledException)
+ {
+ //connection process was cancelled.
+ throw;
+ }
+ catch (Exception e)
+ {
+ await handleErrorAndDelay(e, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+ finally
+ {
+ connectionLock.Release();
+ }
+ }
+
+ ///
+ /// Handles an exception and delays an async flow.
+ ///
+ private async Task handleErrorAndDelay(Exception exception, CancellationToken cancellationToken)
+ {
+ Logger.Log($"{ClientName} connect attempt failed: {exception.Message}", LoggingTarget.Network);
+ await Task.Delay(5000, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Creates a new .
+ ///
+ /// A cancellation token to stop the process.
+ protected abstract Task BuildConnectionAsync(CancellationToken cancellationToken);
+
+ private async Task onConnectionClosed(Exception? ex, CancellationToken cancellationToken)
+ {
+ isConnected.Value = false;
+
+ if (ex != null)
+ await handleErrorAndDelay(ex, cancellationToken).ConfigureAwait(false);
+ else
+ Logger.Log($"{ClientName} disconnected", LoggingTarget.Network);
+
+ // make sure a disconnect wasn't triggered (and this is still the active connection).
+ if (!cancellationToken.IsCancellationRequested)
+ await Task.Run(connect, default).ConfigureAwait(false);
+ }
+
+ private async Task disconnect(bool takeLock)
+ {
+ cancelExistingConnect();
+
+ if (takeLock)
+ {
+ if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false))
+ throw new TimeoutException("Could not obtain a lock to disconnect. A previous attempt is likely stuck.");
+ }
+
+ try
+ {
+ if (CurrentConnection != null)
+ await CurrentConnection.DisposeAsync().ConfigureAwait(false);
+ }
+ finally
+ {
+ CurrentConnection = null;
+ if (takeLock)
+ connectionLock.Release();
+ }
+ }
+
+ private void cancelExistingConnect()
+ {
+ connectCancelSource.Cancel();
+ connectCancelSource = new CancellationTokenSource();
+ }
+
+ protected virtual string ClientName => GetType().ReadableName();
+
+ public override string ToString() => $"{ClientName} ({(IsConnected.Value ? "connected" : "not connected")})";
+
+ private bool isDisposed;
+
+ protected virtual void Dispose(bool isDisposing)
+ {
+ if (isDisposed)
+ return;
+
+ apiState.UnbindAll();
+ cancelExistingConnect();
+
+ isDisposed = true;
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 39ddffd2d0..511f492b8a 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -616,11 +616,16 @@ namespace osu.Game
return;
}
+ var previouslySelectedMods = SelectedMods.Value.ToArray();
+
if (!SelectedMods.Disabled)
SelectedMods.Value = Array.Empty();
AvailableMods.Value = dict;
+ if (!SelectedMods.Disabled)
+ SelectedMods.Value = previouslySelectedMods.Select(m => instance.CreateModFromAcronym(m.Acronym)).Where(m => m != null).ToArray();
+
void revertRulesetChange() => Ruleset.Value = r.OldValue?.Available == true ? r.OldValue : RulesetStore.AvailableRulesets.First();
}
diff --git a/osu.Game/Overlays/Changelog/ChangelogContent.cs b/osu.Game/Overlays/Changelog/ChangelogContent.cs
index 1e49efb10e..e04133f2e4 100644
--- a/osu.Game/Overlays/Changelog/ChangelogContent.cs
+++ b/osu.Game/Overlays/Changelog/ChangelogContent.cs
@@ -1,8 +1,6 @@
// 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 osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
@@ -12,7 +10,7 @@ namespace osu.Game.Overlays.Changelog
{
public class ChangelogContent : FillFlowContainer
{
- public Action BuildSelected;
+ public Action? BuildSelected;
public void SelectBuild(APIChangelogBuild build) => BuildSelected?.Invoke(build);
diff --git a/osu.Game/Overlays/Comments/CancellableCommentEditor.cs b/osu.Game/Overlays/Comments/CancellableCommentEditor.cs
index 853171ea4a..7ba6de86b7 100644
--- a/osu.Game/Overlays/Comments/CancellableCommentEditor.cs
+++ b/osu.Game/Overlays/Comments/CancellableCommentEditor.cs
@@ -12,6 +12,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Comments
@@ -38,6 +39,7 @@ namespace osu.Game.Overlays.Comments
private readonly Box background;
public CancelButton()
+ : base(HoverSampleSet.Button)
{
AutoSizeAxes = Axes.Both;
Child = new CircularContainer
diff --git a/osu.Game/Overlays/Comments/CommentReportButton.cs b/osu.Game/Overlays/Comments/CommentReportButton.cs
new file mode 100644
index 0000000000..4f5c5c6dcf
--- /dev/null
+++ b/osu.Game/Overlays/Comments/CommentReportButton.cs
@@ -0,0 +1,91 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.UserInterface;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Resources.Localisation.Web;
+using osuTK;
+
+namespace osu.Game.Overlays.Comments
+{
+ public class CommentReportButton : CompositeDrawable, IHasPopover
+ {
+ private readonly Comment comment;
+
+ private LinkFlowContainer link = null!;
+ private LoadingSpinner loading = null!;
+
+ [Resolved]
+ private IAPIProvider api { get; set; } = null!;
+
+ [Resolved]
+ private OverlayColourProvider? colourProvider { get; set; }
+
+ public CommentReportButton(Comment comment)
+ {
+ this.comment = comment;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AutoSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ link = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold))
+ {
+ AutoSizeAxes = Axes.Both,
+ },
+ loading = new LoadingSpinner
+ {
+ Size = new Vector2(12f),
+ }
+ };
+
+ link.AddLink(UsersStrings.ReportButtonText, this.ShowPopover);
+ }
+
+ private void report(CommentReportReason reason, string comments)
+ {
+ var request = new CommentReportRequest(comment.Id, reason, comments);
+
+ link.Hide();
+ loading.Show();
+
+ request.Success += () => Schedule(() =>
+ {
+ loading.Hide();
+
+ link.Clear(true);
+ link.AddText(UsersStrings.ReportThanks, s => s.Colour = colourProvider?.Content2 ?? Colour4.White);
+ link.Show();
+
+ this.FadeOut(2000, Easing.InQuint).Expire();
+ });
+
+ request.Failure += _ => Schedule(() =>
+ {
+ loading.Hide();
+ link.Show();
+ });
+
+ api.Queue(request);
+ }
+
+ public Popover GetPopover() => new ReportCommentPopover(comment)
+ {
+ Action = report
+ };
+ }
+}
diff --git a/osu.Game/Overlays/Comments/CommentReportReason.cs b/osu.Game/Overlays/Comments/CommentReportReason.cs
new file mode 100644
index 0000000000..4fbec0164d
--- /dev/null
+++ b/osu.Game/Overlays/Comments/CommentReportReason.cs
@@ -0,0 +1,26 @@
+// 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.Localisation;
+using osu.Game.Resources.Localisation.Web;
+
+namespace osu.Game.Overlays.Comments
+{
+ public enum CommentReportReason
+ {
+ [LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsInsults))]
+ Insults,
+
+ [LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsSpam))]
+ Spam,
+
+ [LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsUnwantedContent))]
+ UnwantedContent,
+
+ [LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsNonsense))]
+ Nonsense,
+
+ [LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.ReportOptionsOther))]
+ Other
+ }
+}
diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs
index 0675d99a25..aa08de798c 100644
--- a/osu.Game/Overlays/Comments/DrawableComment.cs
+++ b/osu.Game/Overlays/Comments/DrawableComment.cs
@@ -31,6 +31,7 @@ using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Comments
{
+ [Cached]
public class DrawableComment : CompositeDrawable
{
private const int avatar_size = 40;
@@ -331,12 +332,12 @@ namespace osu.Game.Overlays.Comments
makeDeleted();
actionsContainer.AddLink("Copy link", copyUrl);
- actionsContainer.AddArbitraryDrawable(new Container { Width = 10 });
+ actionsContainer.AddArbitraryDrawable(Empty().With(d => d.Width = 10));
if (Comment.UserId.HasValue && Comment.UserId.Value == api.LocalUser.Value.Id)
- {
actionsContainer.AddLink("Delete", deleteComment);
- }
+ else
+ actionsContainer.AddArbitraryDrawable(new CommentReportButton(Comment));
if (Comment.IsTopLevel)
{
diff --git a/osu.Game/Overlays/Comments/ReportCommentPopover.cs b/osu.Game/Overlays/Comments/ReportCommentPopover.cs
new file mode 100644
index 0000000000..39fd52aa2a
--- /dev/null
+++ b/osu.Game/Overlays/Comments/ReportCommentPopover.cs
@@ -0,0 +1,111 @@
+// 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.Allocation;
+using osu.Framework.Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Resources.Localisation.Web;
+using osuTK;
+
+namespace osu.Game.Overlays.Comments
+{
+ public class ReportCommentPopover : OsuPopover
+ {
+ public Action? Action;
+
+ private readonly Comment? comment;
+
+ private OsuEnumDropdown reasonDropdown = null!;
+ private OsuTextBox commentsTextBox = null!;
+ private RoundedButton submitButton = null!;
+
+ public ReportCommentPopover(Comment? comment)
+ {
+ this.comment = comment;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ Child = new ReverseChildIDFillFlowContainer
+ {
+ Direction = FillDirection.Vertical,
+ Width = 500,
+ AutoSizeAxes = Axes.Y,
+ Spacing = new Vector2(7),
+ Children = new Drawable[]
+ {
+ new SpriteIcon
+ {
+ Origin = Anchor.TopCentre,
+ Anchor = Anchor.TopCentre,
+ Icon = FontAwesome.Solid.ExclamationTriangle,
+ Size = new Vector2(36),
+ },
+ new OsuSpriteText
+ {
+ Origin = Anchor.TopCentre,
+ Anchor = Anchor.TopCentre,
+ Text = ReportStrings.CommentTitle(comment?.User?.Username ?? comment?.LegacyName ?? @"Someone"),
+ Font = OsuFont.Torus.With(size: 25),
+ Margin = new MarginPadding { Bottom = 10 }
+ },
+ new OsuSpriteText
+ {
+ Origin = Anchor.TopCentre,
+ Anchor = Anchor.TopCentre,
+ Text = UsersStrings.ReportReason,
+ },
+ new Container
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 40,
+ Child = reasonDropdown = new OsuEnumDropdown
+ {
+ RelativeSizeAxes = Axes.X
+ }
+ },
+ new OsuSpriteText
+ {
+ Origin = Anchor.TopCentre,
+ Anchor = Anchor.TopCentre,
+ Text = UsersStrings.ReportComments,
+ },
+ commentsTextBox = new OsuTextBox
+ {
+ RelativeSizeAxes = Axes.X,
+ PlaceholderText = UsersStrings.ReportPlaceholder,
+ },
+ submitButton = new RoundedButton
+ {
+ Origin = Anchor.TopCentre,
+ Anchor = Anchor.TopCentre,
+ Width = 200,
+ BackgroundColour = colours.Red3,
+ Text = UsersStrings.ReportActionsSend,
+ Action = () =>
+ {
+ Action?.Invoke(reasonDropdown.Current.Value, commentsTextBox.Text);
+ this.HidePopover();
+ },
+ Margin = new MarginPadding { Bottom = 5, Top = 10 },
+ }
+ }
+ };
+
+ commentsTextBox.Current.BindValueChanged(e =>
+ {
+ submitButton.Enabled.Value = !string.IsNullOrWhiteSpace(e.NewValue);
+ }, true);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs
index 4cf47013bd..9812feb4a1 100644
--- a/osu.Game/Overlays/Notifications/ProgressNotification.cs
+++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs
@@ -148,7 +148,7 @@ namespace osu.Game.Overlays.Notifications
}
}
- private bool completionSent;
+ private int completionSent;
///
/// Attempt to post a completion notification.
@@ -162,11 +162,11 @@ namespace osu.Game.Overlays.Notifications
if (CompletionTarget == null)
return;
- if (completionSent)
+ // Thread-safe barrier, as this may be called by a web request and also scheduled to the update thread at the same time.
+ if (Interlocked.Exchange(ref completionSent, 1) == 1)
return;
CompletionTarget.Invoke(CreateCompletionNotification());
- completionSent = true;
Close(false);
}
diff --git a/osu.Game/Overlays/OnlineOverlay.cs b/osu.Game/Overlays/OnlineOverlay.cs
index 424584fbcf..24bc7a73e0 100644
--- a/osu.Game/Overlays/OnlineOverlay.cs
+++ b/osu.Game/Overlays/OnlineOverlay.cs
@@ -6,6 +6,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
@@ -45,7 +46,7 @@ namespace osu.Game.Overlays
Children = new Drawable[]
{
Header.With(h => h.Depth = float.MinValue),
- content = new Container
+ content = new PopoverContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
diff --git a/osu.Game/Overlays/Profile/Header/Components/DrawableBadge.cs b/osu.Game/Overlays/Profile/Header/Components/DrawableBadge.cs
index cf75818a0c..2ee80a7494 100644
--- a/osu.Game/Overlays/Profile/Header/Components/DrawableBadge.cs
+++ b/osu.Game/Overlays/Profile/Header/Components/DrawableBadge.cs
@@ -1,22 +1,20 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Localisation;
+using osu.Game.Graphics.Containers;
+using osu.Game.Online;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Overlays.Profile.Header.Components
{
[LongRunningLoad]
- public class DrawableBadge : CompositeDrawable, IHasTooltip
+ public class DrawableBadge : OsuClickableContainer
{
public static readonly Vector2 DRAWABLE_BADGE_SIZE = new Vector2(86, 40);
@@ -29,22 +27,25 @@ namespace osu.Game.Overlays.Profile.Header.Components
}
[BackgroundDependencyLoader]
- private void load(LargeTextureStore textures)
+ private void load(LargeTextureStore textures, ILinkHandler? linkHandler)
{
- InternalChild = new Sprite
+ Child = new Sprite
{
FillMode = FillMode.Fit,
RelativeSizeAxes = Axes.Both,
Texture = textures.Get(badge.ImageUrl),
};
+
+ if (!string.IsNullOrEmpty(badge.Url))
+ Action = () => linkHandler?.HandleLink(badge.Url);
}
protected override void LoadComplete()
{
base.LoadComplete();
- InternalChild.FadeInFromZero(200);
+ this.FadeInFromZero(200);
}
- public LocalisableString TooltipText => badge.Description;
+ public override LocalisableString TooltipText => badge.Description;
}
}
diff --git a/osu.Game/Overlays/Toolbar/ToolbarClock.cs b/osu.Game/Overlays/Toolbar/ToolbarClock.cs
index c5add6eee2..3fd37d9a62 100644
--- a/osu.Game/Overlays/Toolbar/ToolbarClock.cs
+++ b/osu.Game/Overlays/Toolbar/ToolbarClock.cs
@@ -13,6 +13,7 @@ using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
@@ -123,6 +124,8 @@ namespace osu.Game.Overlays.Toolbar
base.OnHoverLost(e);
}
+ protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverClickSounds(sampleSet);
+
private void cycleDisplayMode()
{
switch (clockDisplayMode.Value)
diff --git a/osu.Game/Rulesets/Objects/BezierConverter.cs b/osu.Game/Rulesets/Objects/BezierConverter.cs
new file mode 100644
index 0000000000..ebee36a7db
--- /dev/null
+++ b/osu.Game/Rulesets/Objects/BezierConverter.cs
@@ -0,0 +1,287 @@
+// 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.Utils;
+using osu.Game.Rulesets.Objects.Types;
+using osuTK;
+
+namespace osu.Game.Rulesets.Objects
+{
+ public static class BezierConverter
+ {
+ private struct CircleBezierPreset
+ {
+ public readonly double ArcLength;
+ public readonly Vector2d[] ControlPoints;
+
+ public CircleBezierPreset(double arcLength, Vector2d[] controlPoints)
+ {
+ ArcLength = arcLength;
+ ControlPoints = controlPoints;
+ }
+ }
+
+ // Extremely accurate a bezier anchor positions for approximating circles of several arc lengths
+ private static readonly CircleBezierPreset[] circle_presets =
+ {
+ new CircleBezierPreset(0.4993379862754501,
+ new[] { new Vector2d(1, 0), new Vector2d(1, 0.2549893626632736f), new Vector2d(0.8778997558480327f, 0.47884446188920726f) }),
+ new CircleBezierPreset(1.7579419829169447,
+ new[] { new Vector2d(1, 0), new Vector2d(1, 0.6263026f), new Vector2d(0.42931178f, 1.0990661f), new Vector2d(-0.18605515f, 0.9825393f) }),
+ new CircleBezierPreset(3.1385246920140215,
+ new[] { new Vector2d(1, 0), new Vector2d(1, 0.87084764f), new Vector2d(0.002304826f, 1.5033062f), new Vector2d(-0.9973236f, 0.8739115f), new Vector2d(-0.9999953f, 0.0030679568f) }),
+ new CircleBezierPreset(5.69720464620727,
+ new[] { new Vector2d(1, 0), new Vector2d(1, 1.4137783f), new Vector2d(-1.4305235f, 2.0779421f), new Vector2d(-2.3410065f, -0.94017583f), new Vector2d(0.05132711f, -1.7309346f), new Vector2d(0.8331702f, -0.5530167f) }),
+ new CircleBezierPreset(2 * Math.PI,
+ new[] { new Vector2d(1, 0), new Vector2d(1, 1.2447058f), new Vector2d(-0.8526471f, 2.118367f), new Vector2d(-2.6211002f, 7.854936e-06f), new Vector2d(-0.8526448f, -2.118357f), new Vector2d(1, -1.2447058f), new Vector2d(1, 0) })
+ };
+
+ ///
+ /// Converts a slider path to bezier control point positions compatible with the legacy osu! client.
+ ///
+ /// The control points of the path.
+ /// The offset for the whole path.
+ /// The list of legacy bezier control point positions.
+ public static List ConvertToLegacyBezier(IList controlPoints, Vector2 position)
+ {
+ Vector2[] vertices = new Vector2[controlPoints.Count];
+ for (int i = 0; i < controlPoints.Count; i++)
+ vertices[i] = controlPoints[i].Position;
+
+ var result = new List();
+ int start = 0;
+
+ for (int i = 0; i < controlPoints.Count; i++)
+ {
+ if (controlPoints[i].Type == null && i < controlPoints.Count - 1)
+ continue;
+
+ // The current vertex ends the segment
+ var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1);
+ var segmentType = controlPoints[start].Type ?? PathType.Linear;
+
+ switch (segmentType)
+ {
+ case PathType.Catmull:
+ result.AddRange(from segment in ConvertCatmullToBezierAnchors(segmentVertices) from v in segment select v + position);
+
+ break;
+
+ case PathType.Linear:
+ result.AddRange(from segment in ConvertLinearToBezierAnchors(segmentVertices) from v in segment select v + position);
+
+ break;
+
+ case PathType.PerfectCurve:
+ result.AddRange(ConvertCircleToBezierAnchors(segmentVertices).Select(v => v + position));
+
+ break;
+
+ default:
+ foreach (Vector2 v in segmentVertices)
+ {
+ result.Add(v + position);
+ }
+
+ break;
+ }
+
+ // Start the new segment at the current vertex
+ start = i;
+ }
+
+ return result;
+ }
+
+ ///
+ /// Converts a path of control points to an identical path using only Bezier type control points.
+ ///
+ /// The control points of the path.
+ /// The list of bezier control points.
+ public static List ConvertToModernBezier(IList controlPoints)
+ {
+ Vector2[] vertices = new Vector2[controlPoints.Count];
+ for (int i = 0; i < controlPoints.Count; i++)
+ vertices[i] = controlPoints[i].Position;
+
+ var result = new List();
+ int start = 0;
+
+ for (int i = 0; i < controlPoints.Count; i++)
+ {
+ if (controlPoints[i].Type == null && i < controlPoints.Count - 1)
+ continue;
+
+ // The current vertex ends the segment
+ var segmentVertices = vertices.AsSpan().Slice(start, i - start + 1);
+ var segmentType = controlPoints[start].Type ?? PathType.Linear;
+
+ switch (segmentType)
+ {
+ case PathType.Catmull:
+ foreach (var segment in ConvertCatmullToBezierAnchors(segmentVertices))
+ {
+ for (int j = 0; j < segment.Length - 1; j++)
+ {
+ result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.Bezier : null));
+ }
+ }
+
+ break;
+
+ case PathType.Linear:
+ foreach (var segment in ConvertLinearToBezierAnchors(segmentVertices))
+ {
+ for (int j = 0; j < segment.Length - 1; j++)
+ {
+ result.Add(new PathControlPoint(segment[j], j == 0 ? PathType.Bezier : null));
+ }
+ }
+
+ break;
+
+ case PathType.PerfectCurve:
+ var circleResult = ConvertCircleToBezierAnchors(segmentVertices);
+
+ for (int j = 0; j < circleResult.Length - 1; j++)
+ {
+ result.Add(new PathControlPoint(circleResult[j], j == 0 ? PathType.Bezier : null));
+ }
+
+ break;
+
+ default:
+ for (int j = 0; j < segmentVertices.Length - 1; j++)
+ {
+ result.Add(new PathControlPoint(segmentVertices[j], j == 0 ? PathType.Bezier : null));
+ }
+
+ break;
+ }
+
+ // Start the new segment at the current vertex
+ start = i;
+ }
+
+ result.Add(new PathControlPoint(controlPoints[^1].Position));
+
+ return result;
+ }
+
+ ///
+ /// Converts perfect curve anchors to bezier anchors.
+ ///
+ /// The control point positions to convert.
+ public static Vector2[] ConvertCircleToBezierAnchors(ReadOnlySpan controlPoints)
+ {
+ if (controlPoints.Length != 3)
+ return controlPoints.ToArray();
+
+ var pr = new CircularArcProperties(controlPoints);
+ if (!pr.IsValid)
+ return controlPoints.ToArray();
+
+ CircleBezierPreset preset = circle_presets.Last();
+
+ foreach (CircleBezierPreset cbp in circle_presets)
+ {
+ if (cbp.ArcLength < pr.ThetaRange) continue;
+
+ preset = cbp;
+ break;
+ }
+
+ double arcLength = preset.ArcLength;
+ var arc = new Vector2d[preset.ControlPoints.Length];
+ preset.ControlPoints.CopyTo(arc, 0);
+
+ // Converge on arcLength of thetaRange
+ int n = arc.Length - 1;
+ double tf = pr.ThetaRange / arcLength;
+
+ while (Math.Abs(tf - 1) > 1E-7)
+ {
+ for (int j = 0; j < n; j++)
+ {
+ for (int i = n; i > j; i--)
+ {
+ arc[i] = arc[i] * tf + arc[i - 1] * (1 - tf);
+ }
+ }
+
+ arcLength = Math.Atan2(arc.Last()[1], arc.Last()[0]);
+
+ if (arcLength < 0)
+ {
+ arcLength += 2 * Math.PI;
+ }
+
+ tf = pr.ThetaRange / arcLength;
+ }
+
+ // Adjust rotation, radius, and position
+ var result = new Vector2[arc.Length];
+
+ for (int i = 0; i < arc.Length; i++)
+ {
+ result[i] = new Vector2(
+ (float)((Math.Cos(pr.ThetaStart) * arc[i].X + -Math.Sin(pr.ThetaStart) * pr.Direction * arc[i].Y) * pr.Radius + pr.Centre.X),
+ (float)((Math.Sin(pr.ThetaStart) * arc[i].X + Math.Cos(pr.ThetaStart) * pr.Direction * arc[i].Y) * pr.Radius + pr.Centre.Y));
+ }
+
+ return result;
+ }
+
+ ///
+ /// Converts catmull anchors to bezier anchors.
+ ///
+ /// The control point positions to convert.
+ public static Vector2[][] ConvertCatmullToBezierAnchors(ReadOnlySpan controlPoints)
+ {
+ int iLen = controlPoints.Length;
+ var bezier = new Vector2[iLen - 1][];
+
+ for (int i = 0; i < iLen - 1; i++)
+ {
+ var v1 = i > 0 ? controlPoints[i - 1] : controlPoints[i];
+ var v2 = controlPoints[i];
+ var v3 = i < iLen - 1 ? controlPoints[i + 1] : v2 + v2 - v1;
+ var v4 = i < iLen - 2 ? controlPoints[i + 2] : v3 + v3 - v2;
+
+ bezier[i] = new[]
+ {
+ v2,
+ (-v1 + 6 * v2 + v3) / 6,
+ (-v4 + 6 * v3 + v2) / 6,
+ v3
+ };
+ }
+
+ return bezier;
+ }
+
+ ///
+ /// Converts linear anchors to bezier anchors.
+ ///
+ /// The control point positions to convert.
+ public static Vector2[][] ConvertLinearToBezierAnchors(ReadOnlySpan controlPoints)
+ {
+ int iLen = controlPoints.Length;
+ var bezier = new Vector2[iLen - 1][];
+
+ for (int i = 0; i < iLen - 1; i++)
+ {
+ bezier[i] = new[]
+ {
+ controlPoints[i],
+ controlPoints[i + 1]
+ };
+ }
+
+ return bezier;
+ }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs
index 54914f4b23..bb5b4a6cea 100644
--- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs
+++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs
@@ -57,15 +57,13 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
private void updateRelativeChildSize()
{
- // the track may not be loaded completely (only has a length once it is).
- if (!beatmap.Value.Track.IsLoaded)
- {
- content.RelativeChildSize = Vector2.One;
- Schedule(updateRelativeChildSize);
- return;
- }
+ // If the track is not loaded, assign a default sane length otherwise relative positioning becomes meaningless.
+ double trackLength = beatmap.Value.Track.IsLoaded ? beatmap.Value.Track.Length : 60000;
+ content.RelativeChildSize = new Vector2((float)Math.Max(1, trackLength), 1);
- content.RelativeChildSize = new Vector2((float)Math.Max(1, beatmap.Value.Track.Length), 1);
+ // The track may not be loaded completely (only has a length once it is).
+ if (!beatmap.Value.Track.IsLoaded)
+ Schedule(updateRelativeChildSize);
}
protected virtual void LoadBeatmap(EditorBeatmap beatmap)
diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs
index 6485f683ad..81d82130da 100644
--- a/osu.Game/Screens/Edit/EditorClock.cs
+++ b/osu.Game/Screens/Edit/EditorClock.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Screens.Edit
private readonly Bindable
-
+
@@ -87,6 +87,6 @@
-
+