diff --git a/Gemfile.lock b/Gemfile.lock
index cae682ec2b..07ca3542f9 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -3,25 +3,25 @@ GEM
specs:
CFPropertyList (3.0.5)
rexml
- addressable (2.8.0)
- public_suffix (>= 2.0.2, < 5.0)
+ addressable (2.8.1)
+ public_suffix (>= 2.0.2, < 6.0)
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.2.0)
- aws-partitions (1.601.0)
- aws-sdk-core (3.131.2)
+ aws-partitions (1.653.0)
+ aws-sdk-core (3.166.0)
aws-eventstream (~> 1, >= 1.0.2)
- aws-partitions (~> 1, >= 1.525.0)
- aws-sigv4 (~> 1.1)
+ aws-partitions (~> 1, >= 1.651.0)
+ aws-sigv4 (~> 1.5)
jmespath (~> 1, >= 1.6.1)
- aws-sdk-kms (1.57.0)
- aws-sdk-core (~> 3, >= 3.127.0)
+ aws-sdk-kms (1.59.0)
+ aws-sdk-core (~> 3, >= 3.165.0)
aws-sigv4 (~> 1.1)
- aws-sdk-s3 (1.114.0)
- aws-sdk-core (~> 3, >= 3.127.0)
+ aws-sdk-s3 (1.117.1)
+ aws-sdk-core (~> 3, >= 3.165.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
- aws-sigv4 (1.5.0)
+ aws-sigv4 (1.5.2)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
claide (1.1.0)
@@ -34,10 +34,10 @@ GEM
rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
- dotenv (2.7.6)
+ dotenv (2.8.1)
emoji_regex (3.2.3)
- excon (0.92.3)
- faraday (1.10.0)
+ excon (0.93.1)
+ faraday (1.10.2)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@@ -66,7 +66,7 @@ GEM
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.6)
- fastlane (2.206.2)
+ fastlane (2.210.1)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
@@ -110,9 +110,9 @@ GEM
souyuz (= 0.11.1)
fastlane-plugin-xamarin (0.6.3)
gh_inspector (1.1.3)
- google-apis-androidpublisher_v3 (0.23.0)
- google-apis-core (>= 0.6, < 2.a)
- google-apis-core (0.6.0)
+ google-apis-androidpublisher_v3 (0.29.0)
+ google-apis-core (>= 0.9.0, < 2.a)
+ google-apis-core (0.9.1)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
@@ -121,27 +121,27 @@ GEM
retriable (>= 2.0, < 4.a)
rexml
webrick
- google-apis-iamcredentials_v1 (0.12.0)
- google-apis-core (>= 0.6, < 2.a)
- google-apis-playcustomapp_v1 (0.9.0)
- google-apis-core (>= 0.6, < 2.a)
- google-apis-storage_v1 (0.16.0)
- google-apis-core (>= 0.6, < 2.a)
+ google-apis-iamcredentials_v1 (0.15.0)
+ google-apis-core (>= 0.9.0, < 2.a)
+ google-apis-playcustomapp_v1 (0.12.0)
+ google-apis-core (>= 0.9.1, < 2.a)
+ google-apis-storage_v1 (0.19.0)
+ google-apis-core (>= 0.9.0, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.6.0)
faraday (>= 0.17.3, < 3.0)
- google-cloud-errors (1.2.0)
- google-cloud-storage (1.36.2)
+ google-cloud-errors (1.3.0)
+ google-cloud-storage (1.43.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
- google-apis-storage_v1 (~> 0.1)
+ google-apis-storage_v1 (~> 0.19.0)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
- googleauth (1.2.0)
+ googleauth (1.3.0)
faraday (>= 0.17.3, < 3.a)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
@@ -154,22 +154,22 @@ GEM
httpclient (2.8.3)
jmespath (1.6.1)
json (2.6.2)
- jwt (2.4.1)
+ jwt (2.5.0)
memoist (0.16.2)
mini_magick (4.11.0)
mini_mime (1.1.2)
- mini_portile2 (2.7.1)
+ mini_portile2 (2.8.0)
multi_json (1.15.0)
multipart-post (2.0.0)
nanaimo (0.3.0)
naturally (2.2.1)
- nokogiri (1.13.1)
- mini_portile2 (~> 2.7.0)
+ nokogiri (1.13.9)
+ mini_portile2 (~> 2.8.0)
racc (~> 1.4)
optparse (0.1.1)
os (1.1.4)
plist (3.6.0)
- public_suffix (4.0.7)
+ public_suffix (5.0.0)
racc (1.6.0)
rake (13.0.6)
representable (3.2.0)
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index cc5abf5b03..716115e5c6 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -138,10 +138,10 @@ platform :ios do
end
lane :testflight_prune_dry do
- clean_testflight_testers(days_of_inactivity:45, dry_run: true)
+ clean_testflight_testers(days_of_inactivity:30, dry_run: true)
end
lane :testflight_prune do
- clean_testflight_testers(days_of_inactivity: 45)
+ clean_testflight_testers(days_of_inactivity: 30)
end
end
diff --git a/osu.Android.props b/osu.Android.props
index f251e8ee71..8711ceec64 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,7 +52,7 @@
-
+
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index 3ee1b3da30..09f7292845 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -18,17 +18,9 @@ using osu.Framework;
using osu.Framework.Logging;
using osu.Game.Updater;
using osu.Desktop.Windows;
-using osu.Framework.Input.Handlers;
-using osu.Framework.Input.Handlers.Joystick;
-using osu.Framework.Input.Handlers.Mouse;
-using osu.Framework.Input.Handlers.Tablet;
-using osu.Framework.Input.Handlers.Touch;
using osu.Framework.Threading;
using osu.Game.IO;
using osu.Game.IPC;
-using osu.Game.Overlays.Settings;
-using osu.Game.Overlays.Settings.Sections;
-using osu.Game.Overlays.Settings.Sections.Input;
using osu.Game.Utils;
using SDL2;
@@ -148,27 +140,6 @@ namespace osu.Desktop
desktopWindow.DragDrop += f => fileDrop(new[] { f });
}
- public override SettingsSubsection CreateSettingsSubsectionFor(InputHandler handler)
- {
- switch (handler)
- {
- case ITabletHandler th:
- return new TabletSettings(th);
-
- case MouseHandler mh:
- return new MouseSettings(mh);
-
- case JoystickHandler jh:
- return new JoystickSettings(jh);
-
- case TouchHandler th:
- return new InputSection.HandlerSection(th);
-
- default:
- return base.CreateSettingsSubsectionFor(handler);
- }
- }
-
protected override BatteryInfo CreateBatteryInfo() => new SDL2BatteryInfo();
private readonly List importableFiles = new List();
diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs
index 5c9c95827a..e0f7820262 100644
--- a/osu.Game.Rulesets.Catch/CatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs
@@ -17,6 +17,7 @@ using osu.Game.Rulesets.Catch.Edit;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Catch.Scoring;
+using osu.Game.Rulesets.Catch.Skinning.Argon;
using osu.Game.Rulesets.Catch.Skinning.Legacy;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Difficulty;
@@ -188,6 +189,9 @@ namespace osu.Game.Rulesets.Catch
{
case LegacySkin:
return new CatchLegacySkinTransformer(skin);
+
+ case ArgonSkin:
+ return new CatchArgonSkinTransformer(skin);
}
return null;
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
index 54d50b01c4..220bc49203 100644
--- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
@@ -10,7 +11,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
@@ -23,7 +23,6 @@ using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
-using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
@@ -35,8 +34,6 @@ namespace osu.Game.Rulesets.Catch.Edit
private CatchDistanceSnapGrid distanceSnapGrid;
- private readonly Bindable distanceSnapToggle = new Bindable();
-
private InputManager inputManager;
private readonly BindableDouble timeRangeMultiplier = new BindableDouble(1)
@@ -81,6 +78,19 @@ namespace osu.Game.Rulesets.Catch.Edit
inputManager = GetContainingInputManager();
}
+ protected override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
+ {
+ // osu!catch's distance snap implementation is limited, in that a custom spacing cannot be specified.
+ // Therefore this functionality is not currently used.
+ //
+ // The implementation below is probably correct but should be checked if/when exposed via controls.
+
+ float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
+ float actualDistance = Math.Abs(((CatchHitObject)before).EffectiveX - ((CatchHitObject)after).EffectiveX);
+
+ return actualDistance / expectedDistance;
+ }
+
protected override void Update()
{
base.Update();
@@ -120,11 +130,6 @@ namespace osu.Game.Rulesets.Catch.Edit
new BananaShowerCompositionTool()
};
- protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
- {
- new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
- });
-
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
{
var result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
@@ -196,7 +201,7 @@ namespace osu.Game.Rulesets.Catch.Edit
private void updateDistanceSnapGrid()
{
- if (distanceSnapToggle.Value != TernaryState.True)
+ if (DistanceSnapToggle.Value != TernaryState.True)
{
distanceSnapGrid.Hide();
return;
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs
index fd0ffbd032..ddfbb34435 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs
@@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
public float DisplayRotation => Rotation;
+ public double DisplayStartTime => HitObject.StartTime;
+
///
/// Whether this hit object should stay on the catcher plate when the object is caught by the catcher.
///
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs
index 5de372852b..dd09b6c06d 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs
@@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
public new PalpableCatchHitObject HitObject => (PalpableCatchHitObject)base.HitObject;
+ public double DisplayStartTime => LifetimeStart;
+
Bindable IHasCatchObjectState.AccentColour => AccentColour;
public Bindable HyperDash { get; } = new Bindable();
diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs
index 93c80b09db..f30ef0831a 100644
--- a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs
+++ b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs
@@ -16,6 +16,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
{
PalpableCatchHitObject HitObject { get; }
+ double DisplayStartTime { get; }
+
Bindable AccentColour { get; }
Bindable HyperDash { get; }
diff --git a/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonBananaPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonBananaPiece.cs
new file mode 100644
index 0000000000..9a657c9216
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonBananaPiece.cs
@@ -0,0 +1,122 @@
+// 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.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Catch.Objects;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Skinning.Argon
+{
+ internal class ArgonBananaPiece : ArgonFruitPiece
+ {
+ private Container stabilisedPieceContainer = null!;
+
+ private Drawable fadeContent = null!;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddInternal(fadeContent = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ stabilisedPieceContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ new Circle
+ {
+ Colour = Color4.White.Opacity(0.4f),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Blending = BlendingParameters.Additive,
+ Size = new Vector2(8),
+ Scale = new Vector2(25, 1),
+ },
+ new Box
+ {
+ Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0), Color4.White.Opacity(0.8f)),
+ RelativeSizeAxes = Axes.X,
+ Blending = BlendingParameters.Additive,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.CentreRight,
+ Width = 1.6f,
+ Height = 2,
+ },
+ new Circle
+ {
+ Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0.8f), Color4.White.Opacity(0)),
+ RelativeSizeAxes = Axes.X,
+ Blending = BlendingParameters.Additive,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.CentreLeft,
+ Width = 1.6f,
+ Height = 2,
+ },
+ }
+ },
+ new Circle
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(1.2f),
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Hollow = false,
+ Colour = Color4.White.Opacity(0.1f),
+ Radius = 50,
+ },
+ Child =
+ {
+ Alpha = 0,
+ AlwaysPresent = true,
+ },
+ BorderColour = Color4.White.Opacity(0.1f),
+ BorderThickness = 3,
+ },
+ }
+ });
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ const float parent_scale_application = 0.4f;
+
+ // relative to time on screen
+ const float lens_flare_start = 0.3f;
+ const float lens_flare_end = 0.8f;
+
+ // Undo some of the parent scale being applied to make the lens flare feel a bit better..
+ float scale = parent_scale_application + (1 - parent_scale_application) * (1 / (ObjectState.DisplaySize.X / (CatchHitObject.OBJECT_RADIUS * 2)));
+
+ stabilisedPieceContainer.Rotation = -ObjectState.DisplayRotation;
+ stabilisedPieceContainer.Scale = new Vector2(scale, 1);
+
+ double duration = ObjectState.HitObject.StartTime - ObjectState.DisplayStartTime;
+
+ fadeContent.Alpha = MathHelper.Clamp(
+ Interpolation.ValueAt(
+ Time.Current, 1f, 0f,
+ ObjectState.DisplayStartTime + duration * lens_flare_start,
+ ObjectState.DisplayStartTime + duration * lens_flare_end,
+ Easing.OutQuint
+ ), 0, 1);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonCatcher.cs b/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonCatcher.cs
new file mode 100644
index 0000000000..4db0df4a34
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonCatcher.cs
@@ -0,0 +1,85 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Catch.UI;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Skinning.Argon
+{
+ public class ArgonCatcher : CompositeDrawable
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ new Container
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ RelativeSizeAxes = Axes.X,
+ Height = 10,
+ Children = new Drawable[]
+ {
+ new Circle
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Colour = Color4.White,
+ Width = Catcher.ALLOWED_CATCH_RANGE,
+ },
+ new Box
+ {
+ Name = "long line left",
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreRight,
+ Colour = Color4.White,
+ Alpha = 0.25f,
+ RelativeSizeAxes = Axes.X,
+ Width = 20,
+ Height = 1.8f,
+ },
+ new Circle
+ {
+ Name = "bumper left",
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Colour = Color4.White,
+ RelativeSizeAxes = Axes.X,
+ Width = (1 - Catcher.ALLOWED_CATCH_RANGE) / 2,
+ Height = 4,
+ },
+ new Box
+ {
+ Name = "long line right",
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreLeft,
+ Colour = Color4.White,
+ Alpha = 0.25f,
+ RelativeSizeAxes = Axes.X,
+ Width = 20,
+ Height = 1.8f,
+ },
+ new Circle
+ {
+ Name = "bumper right",
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ Colour = Color4.White,
+ RelativeSizeAxes = Axes.X,
+ Width = (1 - Catcher.ALLOWED_CATCH_RANGE) / 2,
+ Height = 4,
+ },
+ }
+ },
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonDropletPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonDropletPiece.cs
new file mode 100644
index 0000000000..267f8a06a3
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonDropletPiece.cs
@@ -0,0 +1,121 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Catch.Skinning.Default;
+using osu.Game.Rulesets.Catch.UI;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Skinning.Argon
+{
+ internal class ArgonDropletPiece : CatchHitObjectPiece
+ {
+ protected override Drawable HyperBorderPiece => hyperBorderPiece;
+
+ private Drawable hyperBorderPiece = null!;
+
+ private Container layers = null!;
+
+ private float rotationRandomness;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ const float droplet_scale_down = 0.7f;
+
+ int largeBlobSeed = RNG.Next();
+
+ InternalChildren = new[]
+ {
+ new Circle
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(20),
+ },
+ layers = new Container
+ {
+ Scale = new Vector2(droplet_scale_down),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new CircularBlob
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Blending = BlendingParameters.Additive,
+ InnerRadius = 0.5f,
+ Alpha = 0.15f,
+ Seed = largeBlobSeed
+ },
+ new CircularBlob
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Blending = BlendingParameters.Additive,
+ InnerRadius = 0.4f,
+ Alpha = 0.5f,
+ Scale = new Vector2(0.7f),
+ Seed = RNG.Next()
+ },
+ }
+ },
+ hyperBorderPiece = new CircularBlob
+ {
+ Scale = new Vector2(droplet_scale_down),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
+ RelativeSizeAxes = Axes.Both,
+ Blending = BlendingParameters.Additive,
+ InnerRadius = 0.5f,
+ Alpha = 0.15f,
+ Seed = largeBlobSeed
+ },
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ AccentColour.BindValueChanged(colour =>
+ {
+ foreach (var sprite in layers)
+ sprite.Colour = colour.NewValue;
+ }, true);
+
+ rotationRandomness = RNG.NextSingle(0.2f, 1);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ // Note that droplets are rotated at a higher level, so this is mostly just to create more
+ // random arrangements of the multiple layers than actually rotate.
+ //
+ // Because underlying rotation is always clockwise, we apply anti-clockwise resistance to avoid
+ // making things spin too fast.
+ for (int i = 0; i < layers.Count; i++)
+ {
+ layers[i].Rotation -=
+ (float)Clock.ElapsedFrameTime
+ * 0.4f * rotationRandomness
+ // Each layer should alternate rotation speed.
+ * (i % 2 == 1 ? 0.5f : 1);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonFruitPiece.cs
new file mode 100644
index 0000000000..28538d48b3
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonFruitPiece.cs
@@ -0,0 +1,121 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Catch.Skinning.Default;
+using osu.Game.Rulesets.Catch.UI;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Skinning.Argon
+{
+ internal class ArgonFruitPiece : CatchHitObjectPiece
+ {
+ protected override Drawable HyperBorderPiece => hyperBorderPiece;
+
+ private Drawable hyperBorderPiece = null!;
+
+ private Container layers = null!;
+
+ private float rotationRandomness;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ int largeBlobSeed = RNG.Next();
+
+ InternalChildren = new[]
+ {
+ new Circle
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(20),
+ },
+ layers = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new CircularBlob
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Blending = BlendingParameters.Additive,
+ Alpha = 0.15f,
+ InnerRadius = 0.5f,
+ Size = new Vector2(1.1f),
+ Seed = largeBlobSeed,
+ },
+ new CircularBlob
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Blending = BlendingParameters.Additive,
+ InnerRadius = 0.2f,
+ Alpha = 0.5f,
+ Seed = RNG.Next(),
+ },
+ new CircularBlob
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Blending = BlendingParameters.Additive,
+ InnerRadius = 0.05f,
+ Seed = RNG.Next(),
+ },
+ }
+ },
+ hyperBorderPiece = new CircularBlob
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR,
+ RelativeSizeAxes = Axes.Both,
+ Blending = BlendingParameters.Additive,
+ InnerRadius = 0.08f,
+ Size = new Vector2(1.15f),
+ Seed = largeBlobSeed
+ },
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ AccentColour.BindValueChanged(colour =>
+ {
+ foreach (var sprite in layers)
+ sprite.Colour = colour.NewValue;
+ }, true);
+
+ rotationRandomness = RNG.NextSingle(0.2f, 1) * (RNG.NextBool() ? -1 : 1);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ for (int i = 0; i < layers.Count; i++)
+ {
+ layers[i].Rotation +=
+ // Layers are ordered from largest to smallest. Smaller layers should rotate more.
+ (i * 2)
+ * (float)Clock.ElapsedFrameTime
+ * 0.02f * rotationRandomness
+ // Each layer should alternate rotation direction.
+ * (i % 2 == 1 ? 1 : -1);
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonHitExplosion.cs b/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonHitExplosion.cs
new file mode 100644
index 0000000000..90dca49dfd
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonHitExplosion.cs
@@ -0,0 +1,112 @@
+// 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.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Effects;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Catch.Objects;
+using osu.Game.Rulesets.Catch.UI;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Skinning.Argon
+{
+ public class ArgonHitExplosion : CompositeDrawable, IHitExplosion
+ {
+ public override bool RemoveWhenNotAlive => true;
+
+ private Container tallExplosion = null!;
+ private Container largeFaint = null!;
+
+ private readonly Bindable accentColour = new Bindable();
+
+ public ArgonHitExplosion()
+ {
+ Size = new Vector2(20);
+ Anchor = Anchor.BottomCentre;
+ Origin = Anchor.BottomCentre;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChildren = new Drawable[]
+ {
+ tallExplosion = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ Width = 0.1f,
+ Child = new Box
+ {
+ AlwaysPresent = true,
+ Alpha = 0,
+ RelativeSizeAxes = Axes.Both,
+ },
+ },
+ largeFaint = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Masking = true,
+ Child = new Box
+ {
+ AlwaysPresent = true,
+ Alpha = 0,
+ RelativeSizeAxes = Axes.Both,
+ },
+ },
+ };
+
+ accentColour.BindValueChanged(colour =>
+ {
+ tallExplosion.EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = colour.NewValue,
+ Hollow = false,
+ Roundness = 15,
+ Radius = 15,
+ };
+
+ largeFaint.EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Glow,
+ Colour = Interpolation.ValueAt(0.2f, colour.NewValue, Color4.White, 0, 1),
+ Hollow = false,
+ Radius = 50,
+ };
+ }, true);
+ }
+
+ public void Animate(HitExplosionEntry entry)
+ {
+ X = entry.Position;
+ Scale = new Vector2(entry.HitObject.Scale);
+ accentColour.Value = entry.ObjectColour;
+
+ using (BeginAbsoluteSequence(entry.LifetimeStart))
+ {
+ this.FadeOutFromOne(400);
+
+ if (!(entry.HitObject is Droplet))
+ {
+ float scale = Math.Clamp(entry.JudgementResult.ComboAtJudgement / 200f, 0.35f, 1.125f);
+
+ tallExplosion
+ .ScaleTo(new Vector2(1.1f, 20 * scale), 200, Easing.OutQuint)
+ .Then()
+ .ScaleTo(new Vector2(1.1f, 1), 600, Easing.In);
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonJudgementPiece.cs
new file mode 100644
index 0000000000..59e8b5a0b3
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/Argon/ArgonJudgementPiece.cs
@@ -0,0 +1,193 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Utils;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Catch.Skinning.Argon
+{
+ public class ArgonJudgementPiece : CompositeDrawable, IAnimatableJudgement
+ {
+ protected readonly HitResult Result;
+
+ protected SpriteText JudgementText { get; private set; } = null!;
+
+ private RingExplosion? ringExplosion;
+
+ [Resolved]
+ private OsuColour colours { get; set; } = null!;
+
+ public ArgonJudgementPiece(HitResult result)
+ {
+ Result = result;
+ Origin = Anchor.Centre;
+ Y = 160;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AutoSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ JudgementText = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Text = Result.GetDescription().ToUpperInvariant(),
+ Colour = colours.ForHitResult(Result),
+ Blending = BlendingParameters.Additive,
+ Spacing = new Vector2(10, 0),
+ Font = OsuFont.Default.With(size: 28, weight: FontWeight.Regular),
+ },
+ };
+
+ if (Result.IsHit())
+ {
+ AddInternal(ringExplosion = new RingExplosion(Result)
+ {
+ Colour = colours.ForHitResult(Result),
+ });
+ }
+ }
+
+ ///
+ /// Plays the default animation for this judgement piece.
+ ///
+ ///
+ /// The base implementation only handles fade (for all result types) and misses.
+ /// Individual rulesets are recommended to implement their appropriate hit animations.
+ ///
+ public virtual void PlayAnimation()
+ {
+ switch (Result)
+ {
+ default:
+ JudgementText
+ .ScaleTo(Vector2.One)
+ .ScaleTo(new Vector2(1.4f), 1800, Easing.OutQuint);
+ break;
+
+ case HitResult.Miss:
+ this.ScaleTo(1.6f);
+ this.ScaleTo(1, 100, Easing.In);
+
+ this.MoveTo(Vector2.Zero);
+ this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
+
+ this.RotateTo(0);
+ this.RotateTo(40, 800, Easing.InQuint);
+ break;
+ }
+
+ this.FadeOutFromOne(800);
+
+ ringExplosion?.PlayAnimation();
+ }
+
+ public Drawable? GetAboveHitObjectsProxiedContent() => null;
+
+ private class RingExplosion : CompositeDrawable
+ {
+ private readonly float travel = 52;
+
+ public RingExplosion(HitResult result)
+ {
+ const float thickness = 4;
+
+ const float small_size = 9;
+ const float large_size = 14;
+
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ Blending = BlendingParameters.Additive;
+
+ int countSmall = 0;
+ int countLarge = 0;
+
+ switch (result)
+ {
+ case HitResult.Meh:
+ countSmall = 3;
+ travel *= 0.3f;
+ break;
+
+ case HitResult.Ok:
+ case HitResult.Good:
+ countSmall = 4;
+ travel *= 0.6f;
+ break;
+
+ case HitResult.Great:
+ case HitResult.Perfect:
+ countSmall = 4;
+ countLarge = 4;
+ break;
+ }
+
+ for (int i = 0; i < countSmall; i++)
+ AddInternal(new RingPiece(thickness) { Size = new Vector2(small_size) });
+
+ for (int i = 0; i < countLarge; i++)
+ AddInternal(new RingPiece(thickness) { Size = new Vector2(large_size) });
+ }
+
+ public void PlayAnimation()
+ {
+ foreach (var c in InternalChildren)
+ {
+ const float start_position_ratio = 0.3f;
+
+ float direction = RNG.NextSingle(0, 360);
+ float distance = RNG.NextSingle(travel / 2, travel);
+
+ c.MoveTo(new Vector2(
+ MathF.Cos(direction) * distance * start_position_ratio,
+ MathF.Sin(direction) * distance * start_position_ratio
+ ));
+
+ c.MoveTo(new Vector2(
+ MathF.Cos(direction) * distance,
+ MathF.Sin(direction) * distance
+ ), 600, Easing.OutQuint);
+ }
+
+ this.FadeOutFromOne(1000, Easing.OutQuint);
+ }
+
+ public class RingPiece : CircularContainer
+ {
+ public RingPiece(float thickness = 9)
+ {
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ Masking = true;
+ BorderThickness = thickness;
+ BorderColour = Color4.White;
+
+ Child = new Box
+ {
+ AlwaysPresent = true,
+ Alpha = 0,
+ RelativeSizeAxes = Axes.Both
+ };
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Argon/CatchArgonSkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Argon/CatchArgonSkinTransformer.cs
new file mode 100644
index 0000000000..8dae0a2b78
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Skinning/Argon/CatchArgonSkinTransformer.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.Graphics;
+using osu.Game.Skinning;
+
+namespace osu.Game.Rulesets.Catch.Skinning.Argon
+{
+ public class CatchArgonSkinTransformer : SkinTransformer
+ {
+ public CatchArgonSkinTransformer(ISkin skin)
+ : base(skin)
+ {
+ }
+
+ public override Drawable? GetDrawableComponent(ISkinComponent component)
+ {
+ switch (component)
+ {
+ case CatchSkinComponent catchComponent:
+ // TODO: Once everything is finalised, consider throwing UnsupportedSkinComponentException on missing entries.
+ switch (catchComponent.Component)
+ {
+ case CatchSkinComponents.HitExplosion:
+ return new ArgonHitExplosion();
+
+ case CatchSkinComponents.Catcher:
+ return new ArgonCatcher();
+
+ case CatchSkinComponents.Fruit:
+ return new ArgonFruitPiece();
+
+ case CatchSkinComponents.Banana:
+ return new ArgonBananaPiece();
+
+ case CatchSkinComponents.Droplet:
+ return new ArgonDropletPiece();
+ }
+
+ break;
+ }
+
+ return base.GetDrawableComponent(component);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/BananaPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/BananaPiece.cs
index 27252594af..359756f159 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Default/BananaPiece.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Default/BananaPiece.cs
@@ -1,21 +1,19 @@
// 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;
namespace osu.Game.Rulesets.Catch.Skinning.Default
{
public class BananaPiece : CatchHitObjectPiece
{
- protected override BorderPiece BorderPiece { get; }
+ protected override Drawable BorderPiece { get; }
public BananaPiece()
{
RelativeSizeAxes = Axes.Both;
- InternalChildren = new Drawable[]
+ InternalChildren = new[]
{
new BananaPulpFormation
{
diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs
index 6cc5220699..3b8df6ee6f 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs
@@ -7,6 +7,7 @@ using System;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
+using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osuTK.Graphics;
@@ -26,13 +27,13 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
/// A part of this piece that will be faded out while falling in the playfield.
///
[CanBeNull]
- protected virtual BorderPiece BorderPiece => null;
+ protected virtual Drawable BorderPiece => null;
///
/// A part of this piece that will be only visible when is true.
///
[CanBeNull]
- protected virtual HyperBorderPiece HyperBorderPiece => null;
+ protected virtual Drawable HyperBorderPiece => null;
protected override void LoadComplete()
{
diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/DropletPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/DropletPiece.cs
index 6b7f25eed1..b8ae062382 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Default/DropletPiece.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Default/DropletPiece.cs
@@ -11,13 +11,13 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
{
public class DropletPiece : CatchHitObjectPiece
{
- protected override HyperBorderPiece HyperBorderPiece { get; }
+ protected override Drawable HyperBorderPiece { get; }
public DropletPiece()
{
Size = new Vector2(CatchHitObject.OBJECT_RADIUS / 2);
- InternalChildren = new Drawable[]
+ InternalChildren = new[]
{
new Pulp
{
diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs
index 8fb5c8f84a..adee960c3c 100644
--- a/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs
+++ b/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs
@@ -18,14 +18,14 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default
public readonly Bindable VisualRepresentation = new Bindable();
- protected override BorderPiece BorderPiece { get; }
- protected override HyperBorderPiece HyperBorderPiece { get; }
+ protected override Drawable BorderPiece { get; }
+ protected override Drawable HyperBorderPiece { get; }
public FruitPiece()
{
RelativeSizeAxes = Axes.Both;
- InternalChildren = new Drawable[]
+ InternalChildren = new[]
{
new FruitPulpFormation
{
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
index 8f776ff507..0296303867 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
@@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Tests
assertHeadJudgement(HitResult.Miss);
assertTickJudgement(HitResult.LargeTickMiss);
assertTailJudgement(HitResult.Miss);
- assertNoteJudgement(HitResult.IgnoreHit);
+ assertNoteJudgement(HitResult.IgnoreMiss);
}
///
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
index 1f139b5b78..464dbecee5 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs
@@ -76,8 +76,8 @@ namespace osu.Game.Rulesets.Mania.Tests
performTest(objects, new List());
- addJudgementAssert(objects[0], HitResult.IgnoreHit);
- addJudgementAssert(objects[1], HitResult.IgnoreHit);
+ addJudgementAssert(objects[0], HitResult.IgnoreMiss);
+ addJudgementAssert(objects[1], HitResult.IgnoreMiss);
}
[Test]
diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs
index ca9bc89473..2b0098744f 100644
--- a/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs
+++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHoldOff.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override string Acronym => "HO";
- public override double ScoreMultiplier => 1;
+ public override double ScoreMultiplier => 0.9;
public override LocalisableString Description => @"Replaces all hold notes with normal notes.";
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
index 48647f9f5f..14dbc432ff 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs
@@ -69,6 +69,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
///
private double? releaseTime;
+ public override double MaximumJudgementOffset => Tail.MaximumJudgementOffset;
+
public DrawableHoldNote()
: this(null)
{
@@ -260,7 +262,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
tick.MissForcefully();
}
- ApplyResult(r => r.Type = r.Judgement.MaxResult);
+ ApplyResult(r => r.Type = Tail.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult);
endHold();
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
index dc74d38cdc..1b67fc2ca9 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
@@ -4,12 +4,15 @@
#nullable disable
using System;
+using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
+using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Overlays;
@@ -52,6 +55,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
};
private OsuDistanceSnapGrid grid;
+ private SnappingCursorContainer cursor;
public TestSceneOsuDistanceSnapGrid()
{
@@ -88,8 +92,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
RelativeSizeAxes = Axes.Both,
Colour = Color4.SlateGray
},
+ cursor = new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position },
grid = new OsuDistanceSnapGrid(new HitCircle { Position = grid_position }),
- new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position }
};
});
@@ -154,6 +158,37 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertSnappedDistance(expectedDistance);
}
+ [Test]
+ public void TestReferenceObjectNotOnSnapGrid()
+ {
+ AddStep("create grid", () =>
+ {
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.SlateGray
+ },
+ cursor = new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position },
+ grid = new OsuDistanceSnapGrid(new HitCircle
+ {
+ Position = grid_position,
+ // This is important. It sets the reference object to a point in time that isn't on the current snap divisor's grid.
+ // We are testing that the grid's display is offset correctly.
+ StartTime = 40,
+ }),
+ };
+ });
+
+ AddStep("move mouse to point", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2(beat_length, 0) * 2)));
+
+ AddAssert("Ensure cursor is on a grid line", () =>
+ {
+ return grid.ChildrenOfType().Any(p => Precision.AlmostEquals(p.ScreenSpaceDrawQuad.TopRight.X, grid.ToScreenSpace(cursor.LastSnappedPosition).X));
+ });
+ }
+
[Test]
public void TestLimitedDistance()
{
@@ -166,8 +201,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
RelativeSizeAxes = Axes.Both,
Colour = Color4.SlateGray
},
+ cursor = new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position },
grid = new OsuDistanceSnapGrid(new HitCircle { Position = grid_position }, new HitCircle { StartTime = 200 }),
- new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position }
};
});
@@ -186,6 +221,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
public Func GetSnapPosition;
+ public Vector2 LastSnappedPosition { get; private set; }
+
private readonly Drawable cursor;
private InputManager inputManager;
@@ -214,7 +251,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
protected override void Update()
{
base.Update();
- cursor.Position = GetSnapPosition.Invoke(inputManager.CurrentState.Mouse.Position);
+ cursor.Position = LastSnappedPosition = GetSnapPosition.Invoke(inputManager.CurrentState.Mouse.Position);
}
}
}
diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs
index 1e73885540..f9cea5761b 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs
@@ -20,20 +20,49 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
[Test]
- public void TestGridExclusivity()
+ public void TestGridToggles()
{
AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
+
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType().Any());
rectangularGridActive(false);
AddStep("enable rectangular grid", () => InputManager.Key(Key.Y));
- AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType().Any());
+
+ AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
+ AddUntilStep("distance snap grid still visible", () => this.ChildrenOfType().Any());
rectangularGridActive(true);
- AddStep("enable distance snap grid", () => InputManager.Key(Key.T));
+ AddStep("disable distance snap grid", () => InputManager.Key(Key.T));
+ AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType().Any());
AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
+ rectangularGridActive(true);
+
+ AddStep("disable rectangular grid", () => InputManager.Key(Key.Y));
+ AddUntilStep("distance snap grid still hidden", () => !this.ChildrenOfType().Any());
+ rectangularGridActive(false);
+ }
+
+ [Test]
+ public void TestDistanceSnapMomentaryToggle()
+ {
+ AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1)));
+
+ AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType().Any());
+ AddStep("hold alt", () => InputManager.PressKey(Key.AltLeft));
AddUntilStep("distance snap grid visible", () => this.ChildrenOfType().Any());
+ AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
+ AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType().Any());
+ }
+
+ [Test]
+ public void TestGridSnapMomentaryToggle()
+ {
+ rectangularGridActive(false);
+ AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft));
+ rectangularGridActive(true);
+ AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
rectangularGridActive(false);
}
@@ -50,8 +79,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("placement blueprint at (0, 0)", () => Precision.AlmostEquals(Editor.ChildrenOfType().Single().HitObject.Position, new Vector2(0, 0)));
else
AddAssert("placement blueprint at (1, 1)", () => Precision.AlmostEquals(Editor.ChildrenOfType().Single().HitObject.Position, new Vector2(1, 1)));
-
- AddStep("choose selection tool", () => InputManager.Key(Key.Number1));
}
[Test]
diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFreezeFrame.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFreezeFrame.cs
new file mode 100644
index 0000000000..7d7b2d9071
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFreezeFrame.cs
@@ -0,0 +1,22 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Rulesets.Osu.Mods;
+
+namespace osu.Game.Rulesets.Osu.Tests.Mods
+{
+ public class TestSceneOsuModFreezeFrame : OsuModTestScene
+ {
+ [Test]
+ public void TestFreezeFrame()
+ {
+ CreateModTest(new ModTestData
+ {
+ Mod = new OsuModFreezeFrame(),
+ PassCondition = () => true,
+ Autoplay = false,
+ });
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs
index 01d83b55e6..b4727b3c02 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new object[] { LegacyMods.Autoplay, new[] { typeof(OsuModAutoplay) } },
new object[] { LegacyMods.SpunOut, new[] { typeof(OsuModSpunOut) } },
new object[] { LegacyMods.Autopilot, new[] { typeof(OsuModAutopilot) } },
- new object[] { LegacyMods.Target, new[] { typeof(OsuModTarget) } },
+ new object[] { LegacyMods.Target, new[] { typeof(OsuModTargetPractice) } },
new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } }
};
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs
index 1665c40b40..ed1891b7d9 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs
@@ -377,7 +377,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
{
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
- () => judgementResults.Single(r => r.HitObject == hitObject).Type == result);
+ () => judgementResults.Single(r => r.HitObject == hitObject).Type, () => Is.EqualTo(result));
}
private void addJudgementAssert(string name, Func hitObject, HitResult result)
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
index bb967a0a76..da2a6ced67 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs
@@ -40,16 +40,23 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
body.BorderColour = colours.Yellow;
}
+ private int? lastVersion;
+
public override void UpdateFrom(Slider hitObject)
{
base.UpdateFrom(hitObject);
body.PathRadius = hitObject.Scale * OsuHitObject.OBJECT_RADIUS;
- var vertices = new List();
- hitObject.Path.GetPathToProgress(vertices, 0, 1);
+ if (lastVersion != hitObject.Path.Version.Value)
+ {
+ lastVersion = hitObject.Path.Version.Value;
- body.SetVertices(vertices);
+ var vertices = new List();
+ hitObject.Path.GetPathToProgress(vertices, 0, 1);
+
+ body.SetVertices(vertices);
+ }
Size = body.Size;
OriginPosition = body.PathOffset;
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index 265a1d21b1..36ee7c2460 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -59,6 +59,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private readonly BindableList controlPoints = new BindableList();
private readonly IBindable pathVersion = new Bindable();
+ private readonly BindableList selectedObjects = new BindableList();
public SliderSelectionBlueprint(Slider slider)
: base(slider)
@@ -86,6 +87,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
pathVersion.BindValueChanged(_ => editorBeatmap?.Update(HitObject));
BodyPiece.UpdateFrom(HitObject);
+
+ if (editorBeatmap != null)
+ selectedObjects.BindTo(editorBeatmap.SelectedHitObjects);
+ selectedObjects.BindCollectionChanged((_, _) => updateVisualDefinition(), true);
}
public override bool HandleQuickDeletion()
@@ -100,6 +105,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
return true;
}
+ private bool hasSingleObjectSelected => selectedObjects.Count == 1;
+
protected override void Update()
{
base.Update();
@@ -108,14 +115,25 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
BodyPiece.UpdateFrom(HitObject);
}
+ protected override bool OnHover(HoverEvent e)
+ {
+ 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;
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ updateVisualDefinition();
+ base.OnHoverLost(e);
+ }
+
protected override void OnSelected()
{
- AddInternal(ControlPointVisualiser = new PathControlPointVisualiser(HitObject, true)
- {
- RemoveControlPointsRequested = removeControlPoints,
- SplitControlPointsRequested = splitControlPoints
- });
-
+ updateVisualDefinition();
base.OnSelected();
}
@@ -123,13 +141,31 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
base.OnDeselected();
- // throw away frame buffers on deselection.
- ControlPointVisualiser?.Expire();
- ControlPointVisualiser = null;
-
+ updateVisualDefinition();
BodyPiece.RecyclePath();
}
+ 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))
+ {
+ if (ControlPointVisualiser == null)
+ {
+ AddInternal(ControlPointVisualiser = new PathControlPointVisualiser(HitObject, true)
+ {
+ RemoveControlPointsRequested = removeControlPoints,
+ SplitControlPointsRequested = splitControlPoints
+ });
+ }
+ }
+ else
+ {
+ ControlPointVisualiser?.Expire();
+ ControlPointVisualiser = null;
+ }
+ }
+
private Vector2 rightClickPosition;
protected override bool OnMouseDown(MouseDownEvent e)
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs
index 28690ee0b7..b5a13a22ce 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs
@@ -5,19 +5,17 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Rulesets.Osu.Objects;
-using osu.Game.Rulesets.Osu.Skinning.Default;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components
{
public class SpinnerPiece : BlueprintPiece
{
- private readonly CircularContainer circle;
- private readonly RingPiece ring;
+ private readonly Circle circle;
+ private readonly Circle ring;
public SpinnerPiece()
{
@@ -25,18 +23,21 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components
RelativeSizeAxes = Axes.Both;
FillMode = FillMode.Fit;
- Size = new Vector2(1.3f);
+ Size = new Vector2(1);
InternalChildren = new Drawable[]
{
- circle = new CircularContainer
+ circle = new Circle
{
RelativeSizeAxes = Axes.Both,
- Masking = true,
Alpha = 0.5f,
- Child = new Box { RelativeSizeAxes = Axes.Both }
},
- ring = new RingPiece()
+ ring = new Circle
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(OsuHitObject.OBJECT_RADIUS),
+ },
};
}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index 6b4a6e39d9..1460fae4d7 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -13,8 +13,11 @@ using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Utils;
+using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
@@ -44,12 +47,10 @@ namespace osu.Game.Rulesets.Osu.Edit
new SpinnerCompositionTool()
};
- private readonly Bindable distanceSnapToggle = new Bindable();
private readonly Bindable rectangularGridSnapToggle = new Bindable();
protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
{
- new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler }),
new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Th })
});
@@ -80,19 +81,7 @@ namespace osu.Game.Rulesets.Osu.Edit
placementObject = EditorBeatmap.PlacementObject.GetBoundCopy();
placementObject.ValueChanged += _ => updateDistanceSnapGrid();
- distanceSnapToggle.ValueChanged += _ =>
- {
- updateDistanceSnapGrid();
-
- if (distanceSnapToggle.Value == TernaryState.True)
- rectangularGridSnapToggle.Value = TernaryState.False;
- };
-
- rectangularGridSnapToggle.ValueChanged += _ =>
- {
- if (rectangularGridSnapToggle.Value == TernaryState.True)
- distanceSnapToggle.Value = TernaryState.False;
- };
+ DistanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid();
// we may be entering the screen with a selection already active
updateDistanceSnapGrid();
@@ -112,6 +101,14 @@ namespace osu.Game.Rulesets.Osu.Edit
private RectangularPositionSnapGrid rectangularPositionSnapGrid;
+ protected override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
+ {
+ float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
+ float actualDistance = Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position);
+
+ return actualDistance / expectedDistance;
+ }
+
protected override void Update()
{
base.Update();
@@ -132,24 +129,46 @@ namespace osu.Game.Rulesets.Osu.Edit
public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All)
{
if (snapType.HasFlagFast(SnapType.NearbyObjects) && snapToVisibleBlueprints(screenSpacePosition, out var snapResult))
+ {
+ // In the case of snapping to nearby objects, a time value is not provided.
+ // This matches the stable editor (which also uses current time), but with the introduction of time-snapping distance snap
+ // this could result in unexpected behaviour when distance snapping is turned on and a user attempts to place an object that is
+ // BOTH on a valid distance snap ring, and also at the same position as a previous object.
+ //
+ // We want to ensure that in this particular case, the time-snapping component of distance snap is still applied.
+ // The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over
+ // the time value if the proposed positions are roughly the same.
+ if (snapType.HasFlagFast(SnapType.Grids) && DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
+ {
+ (Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition));
+ if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1))
+ snapResult.Time = distanceSnappedTime;
+ }
+
return snapResult;
+ }
+
+ SnapResult result = base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
if (snapType.HasFlagFast(SnapType.Grids))
{
- if (distanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
+ if (DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
{
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
- return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition));
+
+ result.ScreenSpacePosition = distanceSnapGrid.ToScreenSpace(pos);
+ result.Time = time;
}
if (rectangularGridSnapToggle.Value == TernaryState.True)
{
- Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(screenSpacePosition));
- return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition));
+ Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(result.ScreenSpacePosition));
+
+ result.ScreenSpacePosition = rectangularPositionSnapGrid.ToScreenSpace(pos);
}
}
- return base.FindSnappedPositionAndTime(screenSpacePosition, snapType);
+ return result;
}
private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult)
@@ -202,7 +221,7 @@ namespace osu.Game.Rulesets.Osu.Edit
distanceSnapGridCache.Invalidate();
distanceSnapGrid = null;
- if (distanceSnapToggle.Value != TernaryState.True)
+ if (DistanceSnapToggle.Value != TernaryState.True)
return;
switch (BlueprintContainer.CurrentTool)
@@ -229,6 +248,42 @@ namespace osu.Game.Rulesets.Osu.Edit
}
}
+ protected override bool OnKeyDown(KeyDownEvent e)
+ {
+ if (e.Repeat)
+ return false;
+
+ handleToggleViaKey(e);
+ return base.OnKeyDown(e);
+ }
+
+ protected override void OnKeyUp(KeyUpEvent e)
+ {
+ handleToggleViaKey(e);
+ base.OnKeyUp(e);
+ }
+
+ protected override bool AdjustDistanceSpacing(GlobalAction action, float amount)
+ {
+ // To allow better visualisation, ensure that the spacing grid is visible before adjusting.
+ DistanceSnapToggle.Value = TernaryState.True;
+
+ return base.AdjustDistanceSpacing(action, amount);
+ }
+
+ private bool gridSnapMomentary;
+
+ private void handleToggleViaKey(KeyboardEvent key)
+ {
+ bool shiftPressed = key.ShiftPressed;
+
+ if (shiftPressed != gridSnapMomentary)
+ {
+ gridSnapMomentary = shiftPressed;
+ rectangularGridSnapToggle.Value = rectangularGridSnapToggle.Value == TernaryState.False ? TernaryState.True : TernaryState.False;
+ }
+ }
+
private DistanceSnapGrid createDistanceSnapGrid(IEnumerable selectedHitObjects)
{
if (BlueprintContainer.CurrentTool is SpinnerCompositionTool)
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs
index ec93f19e17..f213d9f193 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModApproachDifferent.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override double ScoreMultiplier => 1;
public override IconUsage? Icon { get; } = FontAwesome.Regular.Circle;
- public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles) };
+ public override Type[] IncompatibleMods => new[] { typeof(IHidesApproachCircles), typeof(OsuModFreezeFrame) };
[SettingSource("Initial size", "Change the initial size of the approach circle, relative to hit circles.", 0)]
public BindableFloat Scale { get; } = new BindableFloat(4)
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs
new file mode 100644
index 0000000000..bea5d4f5d9
--- /dev/null
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs
@@ -0,0 +1,89 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using osu.Framework.Graphics;
+using osu.Framework.Localisation;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Osu.Mods
+{
+ public class OsuModFreezeFrame : Mod, IApplicableToDrawableHitObject, IApplicableToBeatmap
+ {
+ public override string Name => "Freeze Frame";
+
+ public override string Acronym => "FR";
+
+ public override double ScoreMultiplier => 1;
+
+ public override LocalisableString Description => "Burn the notes into your memory.";
+
+ //Alters the transforms of the approach circles, breaking the effects of these mods.
+ public override Type[] IncompatibleMods => new[] { typeof(OsuModApproachDifferent) };
+
+ public override ModType Type => ModType.Fun;
+
+ //mod breaks normal approach circle preempt
+ private double originalPreempt;
+
+ public void ApplyToBeatmap(IBeatmap beatmap)
+ {
+ var firstHitObject = beatmap.HitObjects.OfType().FirstOrDefault();
+ if (firstHitObject == null)
+ return;
+
+ double lastNewComboTime = 0;
+
+ originalPreempt = firstHitObject.TimePreempt;
+
+ foreach (var obj in beatmap.HitObjects.OfType())
+ {
+ if (obj.NewCombo) { lastNewComboTime = obj.StartTime; }
+
+ applyFadeInAdjustment(obj);
+ }
+
+ void applyFadeInAdjustment(OsuHitObject osuObject)
+ {
+ osuObject.TimePreempt += osuObject.StartTime - lastNewComboTime;
+
+ foreach (var nested in osuObject.NestedHitObjects.OfType())
+ {
+ switch (nested)
+ {
+ //SliderRepeat wont layer correctly if preempt is changed.
+ case SliderRepeat:
+ break;
+
+ default:
+ applyFadeInAdjustment(nested);
+ break;
+ }
+ }
+ }
+ }
+
+ public void ApplyToDrawableHitObject(DrawableHitObject drawableObject)
+ {
+ drawableObject.ApplyCustomUpdateState += (drawableHitObject, _) =>
+ {
+ if (drawableHitObject is not DrawableHitCircle drawableHitCircle) return;
+
+ var hitCircle = drawableHitCircle.HitObject;
+ var approachCircle = drawableHitCircle.ApproachCircle;
+
+ // Reapply scale, ensuring the AR isn't changed due to the new preempt.
+ approachCircle.ClearTransforms(targetMember: nameof(approachCircle.Scale));
+ approachCircle.ScaleTo(4 * (float)(hitCircle.TimePreempt / originalPreempt));
+
+ using (drawableHitCircle.ApproachCircle.BeginAbsoluteSequence(hitCircle.StartTime - hitCircle.TimePreempt))
+ approachCircle.ScaleTo(1, hitCircle.TimePreempt).Then().Expire();
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
index 618fcfe05d..1621bb50b1 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public override LocalisableString Description => "It never gets boring!";
- public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModTarget)).ToArray();
+ public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModTargetPractice)).ToArray();
[SettingSource("Angle sharpness", "How sharp angles should be", SettingControlType = typeof(SettingsSlider))]
public BindableFloat AngleSharpness { get; } = new BindableFloat(7)
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
index 9708800daa..f691731afe 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Automation;
public override LocalisableString Description => @"Spinners will be automatically completed.";
public override double ScoreMultiplier => 0.9;
- public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot), typeof(OsuModTarget) };
+ public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot), typeof(OsuModTargetPractice) };
public void ApplyToDrawableHitObject(DrawableHitObject hitObject)
{
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs
index 67b19124e1..af37f1e2e5 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.DifficultyIncrease;
public override LocalisableString Description => @"Once you start a slider, follow precisely or get a miss.";
public override double ScoreMultiplier => 1.0;
- public override Type[] IncompatibleMods => new[] { typeof(ModClassic), typeof(OsuModTarget) };
+ public override Type[] IncompatibleMods => new[] { typeof(ModClassic), typeof(OsuModTargetPractice) };
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
{
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs
index 429fe30fc5..b4edb1581e 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModSuddenDeath.cs
@@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
{
typeof(OsuModAutopilot),
- typeof(OsuModTarget),
+ typeof(OsuModTargetPractice),
}).ToArray();
}
}
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs
similarity index 98%
rename from osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs
rename to osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs
index 406968ba08..55c20eebe9 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModTargetPractice.cs
@@ -32,16 +32,15 @@ using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Mods
{
- public class OsuModTarget : ModWithVisibilityAdjustment, IApplicableToDrawableRuleset,
- IApplicableToHealthProcessor, IApplicableToDifficulty, IApplicableFailOverride,
- IHasSeed, IHidesApproachCircles
+ public class OsuModTargetPractice : ModWithVisibilityAdjustment, IApplicableToDrawableRuleset,
+ IApplicableToHealthProcessor, IApplicableToDifficulty, IApplicableFailOverride, IHasSeed, IHidesApproachCircles
{
- public override string Name => "Target";
+ public override string Name => "Target Practice";
public override string Acronym => "TP";
public override ModType Type => ModType.Conversion;
public override IconUsage? Icon => OsuIcon.ModTarget;
public override LocalisableString Description => @"Practice keeping up with the beat of the song.";
- public override double ScoreMultiplier => 1;
+ public override double ScoreMultiplier => 0.1;
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[]
{
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index 23db29b9a6..841a52da7b 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -102,8 +102,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Size = HitArea.DrawSize;
- PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
- StackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
+ PositionBindable.BindValueChanged(_ => UpdatePosition());
+ StackHeightBindable.BindValueChanged(_ => UpdatePosition());
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
}
@@ -134,6 +134,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
}
+ protected virtual void UpdatePosition()
+ {
+ Position = HitObject.StackedPosition;
+ }
+
public override void Shake() => shakeContainer.Shake();
protected override void CheckForResult(bool userTriggered, double timeOffset)
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs
index de6ca7dd38..9966ad3a90 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs
@@ -79,7 +79,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public override void ApplyTransformsAt(double time, bool propagateChildren = false)
{
// For the same reasons as above w.r.t rewinding, we shouldn't propagate to children here either.
- // ReSharper disable once RedundantArgumentDefaultValue - removing the "redundant" default value triggers BaseMethodCallWithDefaultParameter
+
+ // ReSharper disable once RedundantArgumentDefaultValue
base.ApplyTransformsAt(time, false);
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
index 80b9544e5b..d1d749d7e2 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs
@@ -6,7 +6,6 @@
using System;
using System.Diagnostics;
using JetBrains.Annotations;
-using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
@@ -43,13 +42,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
}
- [BackgroundDependencyLoader]
- private void load()
- {
- PositionBindable.BindValueChanged(_ => updatePosition());
- pathVersion.BindValueChanged(_ => updatePosition());
- }
-
protected override void OnFree()
{
base.OnFree();
@@ -57,6 +49,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
pathVersion.UnbindFrom(DrawableSlider.PathVersion);
}
+ protected override void UpdatePosition()
+ {
+ // Slider head is always drawn at (0,0).
+ }
+
protected override void OnApply()
{
base.OnApply();
@@ -100,11 +97,5 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.Shake();
DrawableSlider.Shake();
}
-
- private void updatePosition()
- {
- if (Slider != null)
- Position = HitObject.Position - Slider.Position;
- }
}
}
diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs
index e823053be9..79a566e33c 100644
--- a/osu.Game.Rulesets.Osu/OsuRuleset.cs
+++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs
@@ -109,7 +109,7 @@ namespace osu.Game.Rulesets.Osu
yield return new OsuModSpunOut();
if (mods.HasFlagFast(LegacyMods.Target))
- yield return new OsuModTarget();
+ yield return new OsuModTargetPractice();
if (mods.HasFlagFast(LegacyMods.TouchDevice))
yield return new OsuModTouchDevice();
@@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Osu
value |= LegacyMods.SpunOut;
break;
- case OsuModTarget:
+ case OsuModTargetPractice:
value |= LegacyMods.Target;
break;
@@ -170,7 +170,7 @@ namespace osu.Game.Rulesets.Osu
case ModType.Conversion:
return new Mod[]
{
- new OsuModTarget(),
+ new OsuModTargetPractice(),
new OsuModDifficultyAdjust(),
new OsuModClassic(),
new OsuModRandom(),
@@ -201,7 +201,8 @@ namespace osu.Game.Rulesets.Osu
new OsuModMuted(),
new OsuModNoScope(),
new MultiMod(new OsuModMagnetised(), new OsuModRepel()),
- new ModAdaptiveSpeed()
+ new ModAdaptiveSpeed(),
+ new OsuModFreezeFrame()
};
case ModType.System:
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs
index 36dc8c801d..4ac71e4225 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs
@@ -121,12 +121,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
innerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue.Darken(0.5f), colour.NewValue.Darken(0.6f));
flash.Colour = colour.NewValue;
- updateStateTransforms(drawableObject, drawableObject.State.Value);
+ // Accent colour may be changed many times during a paused gameplay state.
+ // Schedule the change to avoid transforms piling up.
+ Scheduler.AddOnce(updateStateTransforms);
}, true);
drawableObject.ApplyCustomUpdateState += updateStateTransforms;
}
+ private void updateStateTransforms() => updateStateTransforms(drawableObject, drawableObject.State.Value);
+
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
@@ -171,7 +175,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
// This is to give it a bomb-like effect, with the border "triggering" its animation when getting close.
using (BeginDelayedSequence(flash_in_duration / 12))
{
- outerGradient.ResizeTo(outerGradient.Size * shrink_size, resize_duration, Easing.OutElasticHalf);
+ outerGradient.ResizeTo(OUTER_GRADIENT_SIZE * shrink_size, resize_duration, Easing.OutElasticHalf);
outerGradient
.FadeColour(Color4.White, 80)
.Then()
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
index f9f9751b6c..a6e62b83e4 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
@@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
InternalChildren = new[]
{
- CircleSprite = new KiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName) })
+ CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName) })
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- Child = OverlaySprite = new KiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d))
+ Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => skin.GetAnimation(@$"{circleName}overlay", true, true, frameLength: 1000 / 2d))
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModFlashlight.cs
index 417b59f5d2..d55ce17e6c 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModFlashlight.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModFlashlight.cs
@@ -1,8 +1,12 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Linq;
using NUnit.Framework;
+using osu.Framework.Testing;
using osu.Game.Rulesets.Taiko.Mods;
+using osu.Game.Rulesets.Taiko.UI;
+using osuTK;
namespace osu.Game.Rulesets.Taiko.Tests.Mods
{
@@ -16,5 +20,37 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods
[Test]
public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new TaikoModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
+
+ [Test]
+ public void TestFlashlightAlwaysHasNonZeroSize()
+ {
+ bool failed = false;
+
+ CreateModTest(new ModTestData
+ {
+ Mod = new TestTaikoModFlashlight { ComboBasedSize = { Value = true } },
+ Autoplay = false,
+ PassCondition = () =>
+ {
+ failed |= this.ChildrenOfType().SingleOrDefault()?.FlashlightSize.Y == 0;
+ return !failed;
+ }
+ });
+ }
+
+ private class TestTaikoModFlashlight : TaikoModFlashlight
+ {
+ protected override Flashlight CreateFlashlight() => new TestTaikoFlashlight(this, Playfield);
+
+ public class TestTaikoFlashlight : TaikoFlashlight
+ {
+ public TestTaikoFlashlight(TaikoModFlashlight modFlashlight, TaikoPlayfield taikoPlayfield)
+ : base(modFlashlight, taikoPlayfield)
+ {
+ }
+
+ public new Vector2 FlashlightSize => base.FlashlightSize;
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs
index 2d27e0e40e..e42dc254ac 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRoll.cs
@@ -25,8 +25,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
TimeRange = { Value = 5000 },
};
- [BackgroundDependencyLoader]
- private void load()
+ [Test]
+ public void DrumrollTest()
{
AddStep("Drum roll", () => SetContents(_ =>
{
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRollKiai.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRollKiai.cs
new file mode 100644
index 0000000000..53977150e7
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableDrumRollKiai.cs
@@ -0,0 +1,30 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Beatmaps;
+
+namespace osu.Game.Rulesets.Taiko.Tests.Skinning
+{
+ [TestFixture]
+ public class TestSceneDrawableDrumRollKiai : TestSceneDrawableDrumRoll
+ {
+ [SetUp]
+ public void SetUp() => Schedule(() =>
+ {
+ var controlPointInfo = new ControlPointInfo();
+
+ controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
+ controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
+
+ Beatmap.Value = CreateWorkingBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPointInfo
+ });
+
+ // track needs to be playing for BeatSyncedContainer to work.
+ Beatmap.Value.Track.Start();
+ });
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs
index d5a97f8f88..eb2b6c1d74 100644
--- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHit.cs
@@ -4,7 +4,6 @@
#nullable disable
using NUnit.Framework;
-using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@@ -16,8 +15,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
[TestFixture]
public class TestSceneDrawableHit : TaikoSkinnableTestScene
{
- [BackgroundDependencyLoader]
- private void load()
+ [Test]
+ public void TestHits()
{
AddStep("Centre hit", () => SetContents(_ => new DrawableHit(createHitAtCurrentTime())
{
@@ -31,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/TestSceneDrawableHitKiai.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHitKiai.cs
new file mode 100644
index 0000000000..fac0530749
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableHitKiai.cs
@@ -0,0 +1,30 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+
+namespace osu.Game.Rulesets.Taiko.Tests.Skinning
+{
+ [TestFixture]
+ public class TestSceneDrawableHitKiai : TestSceneDrawableHit
+ {
+ [SetUp]
+ public void SetUp() => Schedule(() =>
+ {
+ var controlPointInfo = new ControlPointInfo();
+
+ controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
+ controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
+
+ Beatmap.Value = CreateWorkingBeatmap(new Beatmap
+ {
+ ControlPointInfo = controlPointInfo
+ });
+
+ // track needs to be playing for BeatSyncedContainer to work.
+ Beatmap.Value.Track.Start();
+ });
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.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/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index 524565a863..3cc47deed0 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -57,6 +57,28 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
Beatmap converted = base.ConvertBeatmap(original, cancellationToken);
+ if (original.BeatmapInfo.Ruleset.OnlineID == 0)
+ {
+ // Post processing step to transform standard slider velocity changes into scroll speed changes
+ double lastScrollSpeed = 1;
+
+ foreach (HitObject hitObject in original.HitObjects)
+ {
+ double nextScrollSpeed = hitObject.DifficultyControlPoint.SliderVelocity;
+ EffectControlPoint currentEffectPoint = converted.ControlPointInfo.EffectPointAt(hitObject.StartTime);
+
+ if (!Precision.AlmostEquals(lastScrollSpeed, nextScrollSpeed, acceptableDifference: currentEffectPoint.ScrollSpeedBindable.Precision))
+ {
+ converted.ControlPointInfo.Add(hitObject.StartTime, new EffectControlPoint
+ {
+ KiaiMode = currentEffectPoint.KiaiMode,
+ OmitFirstBarLine = currentEffectPoint.OmitFirstBarLine,
+ ScrollSpeed = lastScrollSpeed = nextScrollSpeed,
+ });
+ }
+ }
+ }
+
if (original.BeatmapInfo.Ruleset.OnlineID == 3)
{
// Post processing step to transform mania hit objects with the same start time into strong hits
diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
index df1450bf77..863a2c9eac 100644
--- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
@@ -27,6 +27,12 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
};
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ BeginPlacement();
+ }
+
protected override bool OnMouseDown(MouseDownEvent e)
{
switch (e.Button)
diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs
index 23a005190a..70364cabf1 100644
--- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs
@@ -52,6 +52,12 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
private double originalStartTime;
private Vector2 originalPosition;
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ BeginPlacement();
+ }
+
protected override bool OnMouseDown(MouseDownEvent e)
{
if (e.Button != MouseButton.Left)
diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs
index 1c1a5c325f..161799c980 100644
--- a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.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.Edit;
using osu.Game.Rulesets.Edit.Tools;
diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
index 98f954ad29..46569c2495 100644
--- a/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
+++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModFlashlight.cs
@@ -27,17 +27,17 @@ namespace osu.Game.Rulesets.Taiko.Mods
public override float DefaultFlashlightSize => 200;
- protected override Flashlight CreateFlashlight() => new TaikoFlashlight(this, playfield);
+ protected override Flashlight CreateFlashlight() => new TaikoFlashlight(this, Playfield);
- private TaikoPlayfield playfield = null!;
+ protected TaikoPlayfield Playfield { get; private set; } = null!;
public override void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
- playfield = (TaikoPlayfield)drawableRuleset.Playfield;
+ Playfield = (TaikoPlayfield)drawableRuleset.Playfield;
base.ApplyToDrawableRuleset(drawableRuleset);
}
- private class TaikoFlashlight : Flashlight
+ public class TaikoFlashlight : Flashlight
{
private readonly LayoutValue flashlightProperties = new LayoutValue(Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo);
private readonly TaikoPlayfield taikoPlayfield;
@@ -47,21 +47,28 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
this.taikoPlayfield = taikoPlayfield;
- FlashlightSize = adjustSize(GetSize());
+ FlashlightSize = adjustSizeForPlayfieldAspectRatio(GetSize());
FlashlightSmoothness = 1.4f;
AddLayout(flashlightProperties);
}
- private Vector2 adjustSize(float size)
+ ///
+ /// Returns the aspect ratio-adjusted size of the flashlight.
+ /// This ensures that the size of the flashlight remains independent of taiko-specific aspect ratio adjustments.
+ ///
+ ///
+ /// The size of the flashlight.
+ /// The value provided here should always come from .
+ ///
+ private Vector2 adjustSizeForPlayfieldAspectRatio(float size)
{
- // Preserve flashlight size through the playfield's aspect adjustment.
return new Vector2(0, size * taikoPlayfield.DrawHeight / TaikoPlayfield.DEFAULT_HEIGHT);
}
protected override void UpdateFlashlightSize(float size)
{
- this.TransformTo(nameof(FlashlightSize), adjustSize(size), FLASHLIGHT_FADE_DURATION);
+ this.TransformTo(nameof(FlashlightSize), adjustSizeForPlayfieldAspectRatio(size), FLASHLIGHT_FADE_DURATION);
}
protected override string FragmentShader => "CircularFlashlight";
@@ -75,7 +82,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
FlashlightPosition = ToLocalSpace(taikoPlayfield.HitTarget.ScreenSpaceDrawQuad.Centre);
ClearTransforms(targetMember: nameof(FlashlightSize));
- FlashlightSize = adjustSize(Combo.Value);
+ FlashlightSize = adjustSizeForPlayfieldAspectRatio(GetSize());
flashlightProperties.Validate();
}
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/Skinning/Default/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs
index a7ab1bcd4a..6b5a9ae6d2 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs
@@ -3,6 +3,7 @@
#nullable disable
+using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@@ -13,6 +14,7 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
+using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects;
using osuTK.Graphics;
@@ -32,6 +34,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
private const double pre_beat_transition_time = 80;
+ private const float flash_opacity = 0.3f;
+
private Color4 accentColour;
///
@@ -152,11 +156,22 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
};
}
+ [Resolved]
+ private DrawableHitObject drawableHitObject { get; set; }
+
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
if (!effectPoint.KiaiMode)
return;
+ if (drawableHitObject.State.Value == ArmedState.Idle)
+ {
+ FlashBox
+ .FadeTo(flash_opacity)
+ .Then()
+ .FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine);
+ }
+
if (beatIndex % timingPoint.TimeSignature.Numerator != 0)
return;
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 399bd9260d..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";
@@ -45,7 +43,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
}
// backgroundLayer is guaranteed to exist due to the pre-check in TaikoLegacySkinTransformer.
- AddInternal(backgroundLayer = getDrawableFor("circle"));
+ AddInternal(backgroundLayer = new LegacyKiaiFlashingDrawable(() => getDrawableFor("circle")));
var foregroundLayer = getDrawableFor("circleoverlay");
if (foregroundLayer != null)
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/Collections/IO/ImportCollectionsTest.cs b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs
index 604b87dc4c..9079ecdc48 100644
--- a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs
+++ b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs
@@ -188,7 +188,7 @@ namespace osu.Game.Tests.Collections.IO
}
// Name matches the automatically chosen name from `CleanRunHeadlessGameHost` above, so we end up using the same storage location.
- using (HeadlessGameHost host = new TestRunHeadlessGameHost(firstRunName, null))
+ using (HeadlessGameHost host = new TestRunHeadlessGameHost(firstRunName))
{
try
{
diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
index 1e87ed27df..495a221159 100644
--- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
+++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs
@@ -7,6 +7,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
+using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
@@ -66,12 +67,25 @@ namespace osu.Game.Tests.Editing
{
AddStep($"set slider multiplier = {multiplier}", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = multiplier);
- assertSnapDistance(100 * multiplier);
+ assertSnapDistance(100 * multiplier, null, true);
}
[TestCase(1)]
[TestCase(2)]
- public void TestSpeedMultiplier(float multiplier)
+ public void TestSpeedMultiplierDoesNotChangeDistanceSnap(float multiplier)
+ {
+ assertSnapDistance(100, new HitObject
+ {
+ DifficultyControlPoint = new DifficultyControlPoint
+ {
+ SliderVelocity = multiplier
+ }
+ }, false);
+ }
+
+ [TestCase(1)]
+ [TestCase(2)]
+ public void TestSpeedMultiplierDoesChangeDistanceSnap(float multiplier)
{
assertSnapDistance(100 * multiplier, new HitObject
{
@@ -79,7 +93,7 @@ namespace osu.Game.Tests.Editing
{
SliderVelocity = multiplier
}
- });
+ }, true);
}
[TestCase(1)]
@@ -88,7 +102,32 @@ namespace osu.Game.Tests.Editing
{
AddStep($"set divisor = {divisor}", () => BeatDivisor.Value = divisor);
- assertSnapDistance(100f / divisor);
+ assertSnapDistance(100f / divisor, null, true);
+ }
+
+ ///
+ /// The basic distance-duration functions should always include slider velocity of the reference object.
+ ///
+ [Test]
+ public void TestConversionsWithSliderVelocity()
+ {
+ const float base_distance = 100;
+ const float slider_velocity = 1.2f;
+
+ var referenceObject = new HitObject
+ {
+ DifficultyControlPoint = new DifficultyControlPoint
+ {
+ SliderVelocity = slider_velocity
+ }
+ };
+
+ assertSnapDistance(base_distance * slider_velocity, referenceObject, true);
+ assertSnappedDistance(base_distance * slider_velocity + 10, base_distance * slider_velocity, referenceObject);
+ assertSnappedDuration(base_distance * slider_velocity + 10, 1000, referenceObject);
+
+ assertDistanceToDuration(base_distance * slider_velocity, 1000, referenceObject);
+ assertDurationToDistance(1000, base_distance * slider_velocity, referenceObject);
}
[Test]
@@ -197,20 +236,20 @@ namespace osu.Game.Tests.Editing
assertSnappedDistance(400, 400);
}
- private void assertSnapDistance(float expectedDistance, HitObject? hitObject = null)
- => AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(hitObject ?? new HitObject()), () => Is.EqualTo(expectedDistance));
+ private void assertSnapDistance(float expectedDistance, HitObject? referenceObject, bool includeSliderVelocity)
+ => AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(referenceObject ?? new HitObject(), includeSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
- private void assertDurationToDistance(double duration, float expectedDistance)
- => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(new HitObject(), duration) == expectedDistance);
+ private void assertDurationToDistance(double duration, float expectedDistance, HitObject? referenceObject = null)
+ => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(referenceObject ?? new HitObject(), duration), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
- private void assertDistanceToDuration(float distance, double expectedDuration)
- => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceToDuration(new HitObject(), distance) == expectedDuration);
+ private void assertDistanceToDuration(float distance, double expectedDuration, HitObject? referenceObject = null)
+ => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceToDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON));
- private void assertSnappedDuration(float distance, double expectedDuration)
- => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.FindSnappedDuration(new HitObject(), distance) == expectedDuration);
+ private void assertSnappedDuration(float distance, double expectedDuration, HitObject? referenceObject = null)
+ => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.FindSnappedDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON));
- private void assertSnappedDistance(float distance, float expectedDistance)
- => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.FindSnappedDistance(new HitObject(), distance) == expectedDistance);
+ private void assertSnappedDistance(float distance, float expectedDistance, HitObject? referenceObject = null)
+ => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.FindSnappedDistance(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
private class TestHitObjectComposer : OsuHitObjectComposer
{
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs
index ecd7732862..f2d27b9117 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneBeatDivisorControl.cs
@@ -106,6 +106,49 @@ namespace osu.Game.Tests.Visual.Editing
assertBeatSnap(16);
}
+ [Test]
+ public void TestKeyboardNavigation()
+ {
+ pressKey(1);
+ assertBeatSnap(1);
+ assertPreset(BeatDivisorType.Common);
+
+ pressKey(2);
+ assertBeatSnap(2);
+ assertPreset(BeatDivisorType.Common);
+
+ pressKey(3);
+ assertBeatSnap(3);
+ assertPreset(BeatDivisorType.Triplets);
+
+ pressKey(4);
+ assertBeatSnap(4);
+ assertPreset(BeatDivisorType.Common);
+
+ pressKey(5);
+ assertBeatSnap(5);
+ assertPreset(BeatDivisorType.Custom, 5);
+
+ pressKey(6);
+ assertBeatSnap(6);
+ assertPreset(BeatDivisorType.Triplets);
+
+ pressKey(7);
+ assertBeatSnap(7);
+ assertPreset(BeatDivisorType.Custom, 7);
+
+ pressKey(8);
+ assertBeatSnap(8);
+ assertPreset(BeatDivisorType.Common);
+
+ void pressKey(int key) => AddStep($"press shift+{key}", () =>
+ {
+ InputManager.PressKey(Key.ShiftLeft);
+ InputManager.Key(Key.Number0 + key);
+ InputManager.ReleaseKey(Key.ShiftLeft);
+ });
+ }
+
[Test]
public void TestBeatPresetNavigation()
{
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
index 53b6db2277..01a49c7dea 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
@@ -193,7 +193,7 @@ namespace osu.Game.Tests.Visual.Editing
IBindable IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier;
- public float GetBeatSnapDistanceAt(HitObject referenceObject) => beat_snap_distance;
+ public float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true) => beat_snap_distance;
public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration;
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBindings.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBindings.cs
new file mode 100644
index 0000000000..5771d64775
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBindings.cs
@@ -0,0 +1,32 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Editing
+{
+ ///
+ /// Test editor hotkeys at a high level to ensure they all work well together.
+ ///
+ public class TestSceneEditorBindings : EditorTestScene
+ {
+ protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
+
+ [Test]
+ public void TestBeatDivisorChangeHotkeys()
+ {
+ AddStep("hold shift", () => InputManager.PressKey(Key.LShift));
+
+ AddStep("press 4", () => InputManager.Key(Key.Number4));
+ AddAssert("snap updated to 4", () => EditorBeatmap.BeatmapInfo.BeatDivisor, () => Is.EqualTo(4));
+
+ AddStep("press 6", () => InputManager.Key(Key.Number6));
+ AddAssert("snap updated to 6", () => EditorBeatmap.BeatmapInfo.BeatDivisor, () => Is.EqualTo(6));
+
+ AddStep("release shift", () => InputManager.ReleaseKey(Key.LShift));
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs
index 23e137865c..0a5a1febe4 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs
@@ -155,6 +155,20 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("composer selection box is visible", () => Editor.ChildrenOfType().First().ChildrenOfType().First().Alpha > 0);
}
+ [Test]
+ public void TestClone()
+ {
+ var addedObject = new HitCircle { StartTime = 1000 };
+ AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
+ AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
+
+ AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1);
+ AddStep("clone", () => Editor.Clone());
+ AddAssert("is two objects", () => EditorBeatmap.HitObjects.Count == 2);
+ AddStep("clone", () => Editor.Clone());
+ AddAssert("is three objects", () => EditorBeatmap.HitObjects.Count == 3);
+ }
+
[Test]
public void TestCutNothing()
{
@@ -175,5 +189,22 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("paste hitobject", () => Editor.Paste());
AddAssert("are no objects", () => EditorBeatmap.HitObjects.Count == 0);
}
+
+ [Test]
+ public void TestCloneNothing()
+ {
+ // Add arbitrary object and copy to clipboard.
+ // This is tested to ensure that clone doesn't incorrectly read from the clipboard when no selection is made.
+ var addedObject = new HitCircle { StartTime = 1000 };
+ AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
+ AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
+ AddStep("copy hitobject", () => Editor.Copy());
+
+ AddStep("deselect all objects", () => EditorBeatmap.SelectedHitObjects.Clear());
+
+ AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1);
+ AddStep("clone", () => Editor.Clone());
+ AddAssert("still one object", () => EditorBeatmap.HitObjects.Count == 1);
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs
index 58b5b41702..1c87eb49c9 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs
@@ -161,10 +161,11 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("hold alt", () => InputManager.PressKey(Key.LAlt));
AddStep("scroll mouse 5 steps", () => InputManager.ScrollVerticalBy(5));
- AddAssert("distance spacing increased by 0.5", () => editorBeatmap.BeatmapInfo.DistanceSpacing == originalSpacing + 0.5);
AddStep("release alt", () => InputManager.ReleaseKey(Key.LAlt));
AddStep("release ctrl", () => InputManager.ReleaseKey(Key.LControl));
+
+ AddAssert("distance spacing increased by 0.5", () => editorBeatmap.BeatmapInfo.DistanceSpacing == originalSpacing + 0.5);
}
public class EditorBeatmapContainer : Container
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
index a984f508ea..75510fa822 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs
@@ -1,17 +1,17 @@
// 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 System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Configuration;
+using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
@@ -26,9 +26,9 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneHUDOverlay : OsuManualInputManagerTestScene
{
- private OsuConfigManager localConfig;
+ private OsuConfigManager localConfig = null!;
- private HUDOverlay hudOverlay;
+ private HUDOverlay hudOverlay = null!;
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
@@ -149,6 +149,41 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent);
}
+ [Test]
+ public void TestHoldForMenuDoesWorkWhenHidden()
+ {
+ bool activated = false;
+
+ HoldForMenuButton getHoldForMenu() => hudOverlay.ChildrenOfType().Single();
+
+ createNew();
+
+ AddStep("bind action", () =>
+ {
+ activated = false;
+
+ var holdForMenu = getHoldForMenu();
+
+ holdForMenu.Action += () => activated = true;
+ });
+
+ AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
+ AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent);
+
+ AddStep("attempt activate", () =>
+ {
+ InputManager.MoveMouseTo(getHoldForMenu().OfType().Single());
+ InputManager.PressButton(MouseButton.Left);
+ });
+
+ AddUntilStep("activated", () => activated);
+
+ AddStep("release mouse button", () =>
+ {
+ InputManager.ReleaseButton(MouseButton.Left);
+ });
+ }
+
[Test]
public void TestInputDoesntWorkWhenHUDHidden()
{
@@ -220,7 +255,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("skinnable components loaded", () => hudOverlay.ChildrenOfType().Single().ComponentsLoaded);
}
- private void createNew(Action action = null)
+ private void createNew(Action? action = null)
{
AddStep("create overlay", () =>
{
@@ -239,7 +274,9 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override void Dispose(bool isDisposing)
{
- localConfig?.Dispose();
+ if (localConfig.IsNotNull())
+ localConfig.Dispose();
+
base.Dispose(isDisposing);
}
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs
new file mode 100644
index 0000000000..f8b5085a70
--- /dev/null
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScoring.cs
@@ -0,0 +1,498 @@
+// 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 NUnit.Framework;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Input.Events;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays.Settings;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Beatmaps;
+using osu.Game.Rulesets.Osu.Judgements;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Scoring;
+using osuTK;
+using osuTK.Graphics;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Gameplay
+{
+ public class TestSceneScoring : OsuTestScene
+ {
+ private GraphContainer graphs = null!;
+ private SettingsSlider sliderMaxCombo = null!;
+
+ private FillFlowContainer legend = null!;
+
+ [Test]
+ public void TestBasic()
+ {
+ AddStep("setup tests", () =>
+ {
+ Children = new Drawable[]
+ {
+ new GridContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ RowDimensions = new[]
+ {
+ new Dimension(),
+ new Dimension(GridSizeMode.AutoSize),
+ new Dimension(GridSizeMode.AutoSize),
+ },
+ Content = new[]
+ {
+ new Drawable[]
+ {
+ graphs = new GraphContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ },
+ new Drawable[]
+ {
+ legend = new FillFlowContainer
+ {
+ Padding = new MarginPadding(20),
+ Direction = FillDirection.Full,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ },
+ },
+ new Drawable[]
+ {
+ new FillFlowContainer
+ {
+ Padding = new MarginPadding(20),
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Full,
+ Children = new Drawable[]
+ {
+ sliderMaxCombo = new SettingsSlider
+ {
+ Width = 0.5f,
+ TransferValueOnCommit = true,
+ Current = new BindableInt(1024)
+ {
+ MinValue = 96,
+ MaxValue = 8192,
+ },
+ LabelText = "max combo",
+ },
+ new OsuTextFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ Width = 0.5f,
+ AutoSizeAxes = Axes.Y,
+ Text = $"Left click to add miss\nRight click to add OK/{base_ok}"
+ }
+ }
+ },
+ },
+ }
+ }
+ };
+
+ sliderMaxCombo.Current.BindValueChanged(_ => rerun());
+
+ graphs.MissLocations.BindCollectionChanged((_, __) => rerun());
+ graphs.NonPerfectLocations.BindCollectionChanged((_, __) => rerun());
+
+ graphs.MaxCombo.BindTo(sliderMaxCombo.Current);
+
+ rerun();
+ });
+ }
+
+ private const int base_great = 300;
+ private const int base_ok = 100;
+
+ private void rerun()
+ {
+ graphs.Clear();
+ legend.Clear();
+
+ runForProcessor("lazer-standardised", Color4.YellowGreen, new ScoreProcessor(new OsuRuleset()) { Mode = { Value = ScoringMode.Standardised } });
+ runForProcessor("lazer-classic", Color4.MediumPurple, new ScoreProcessor(new OsuRuleset()) { Mode = { Value = ScoringMode.Classic } });
+
+ runScoreV1();
+ runScoreV2();
+ }
+
+ private void runScoreV1()
+ {
+ int totalScore = 0;
+ int currentCombo = 0;
+
+ void applyHitV1(int baseScore)
+ {
+ if (baseScore == 0)
+ {
+ currentCombo = 0;
+ return;
+ }
+
+ const float score_multiplier = 1;
+
+ totalScore += baseScore;
+
+ // combo multiplier
+ // ReSharper disable once PossibleLossOfFraction
+ totalScore += (int)(Math.Max(0, currentCombo - 1) * (baseScore / 25 * score_multiplier));
+
+ currentCombo++;
+ }
+
+ runForAlgorithm("ScoreV1 (classic)", Color4.Purple,
+ () => applyHitV1(base_great),
+ () => applyHitV1(base_ok),
+ () => applyHitV1(0),
+ () =>
+ {
+ // Arbitrary value chosen towards the upper range.
+ const double score_multiplier = 4;
+
+ return (int)(totalScore * score_multiplier);
+ });
+ }
+
+ private void runScoreV2()
+ {
+ int maxCombo = sliderMaxCombo.Current.Value;
+
+ int currentCombo = 0;
+ double comboPortion = 0;
+ double currentBaseScore = 0;
+ double maxBaseScore = 0;
+ int currentHits = 0;
+
+ for (int i = 0; i < maxCombo; i++)
+ applyHitV2(base_great);
+
+ double comboPortionMax = comboPortion;
+
+ currentCombo = 0;
+ comboPortion = 0;
+ currentBaseScore = 0;
+ maxBaseScore = 0;
+ currentHits = 0;
+
+ void applyHitV2(int baseScore)
+ {
+ maxBaseScore += base_great;
+ currentBaseScore += baseScore;
+ comboPortion += baseScore * (1 + ++currentCombo / 10.0);
+
+ currentHits++;
+ }
+
+ runForAlgorithm("ScoreV2", Color4.OrangeRed,
+ () => applyHitV2(base_great),
+ () => applyHitV2(base_ok),
+ () =>
+ {
+ currentHits++;
+ maxBaseScore += base_great;
+ currentCombo = 0;
+ }, () =>
+ {
+ double accuracy = currentBaseScore / maxBaseScore;
+
+ return (int)Math.Round
+ (
+ 700000 * comboPortion / comboPortionMax +
+ 300000 * Math.Pow(accuracy, 10) * ((double)currentHits / maxCombo)
+ );
+ });
+ }
+
+ private void runForProcessor(string name, Color4 colour, ScoreProcessor processor)
+ {
+ int maxCombo = sliderMaxCombo.Current.Value;
+
+ var beatmap = new OsuBeatmap();
+ for (int i = 0; i < maxCombo; i++)
+ beatmap.HitObjects.Add(new HitCircle());
+
+ processor.ApplyBeatmap(beatmap);
+
+ runForAlgorithm(name, colour,
+ () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }),
+ () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }),
+ () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }),
+ () => (int)processor.TotalScore.Value);
+ }
+
+ private void runForAlgorithm(string name, Color4 colour, Action applyHit, Action applyNonPerfect, Action applyMiss, Func getTotalScore)
+ {
+ int maxCombo = sliderMaxCombo.Current.Value;
+
+ List results = new List();
+
+ for (int i = 0; i < maxCombo; i++)
+ {
+ if (graphs.MissLocations.Contains(i))
+ applyMiss();
+ else if (graphs.NonPerfectLocations.Contains(i))
+ applyNonPerfect();
+ else
+ applyHit();
+
+ results.Add(getTotalScore());
+ }
+
+ graphs.Add(new LineGraph
+ {
+ Name = name,
+ RelativeSizeAxes = Axes.Both,
+ LineColour = colour,
+ Values = results
+ });
+
+ legend.Add(new OsuSpriteText
+ {
+ Colour = colour,
+ RelativeSizeAxes = Axes.X,
+ Width = 0.5f,
+ Text = $"{FontAwesome.Solid.Circle.Icon} {name}"
+ });
+
+ legend.Add(new OsuSpriteText
+ {
+ Colour = colour,
+ RelativeSizeAxes = Axes.X,
+ Width = 0.5f,
+ Text = $"final score {getTotalScore():#,0}"
+ });
+ }
+ }
+
+ public class GraphContainer : Container, IHasCustomTooltip>
+ {
+ public readonly BindableList MissLocations = new BindableList();
+ public readonly BindableList NonPerfectLocations = new BindableList();
+
+ public Bindable MaxCombo = new Bindable();
+
+ protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
+
+ private readonly Box hoverLine;
+
+ private readonly Container missLines;
+ private readonly Container verticalGridLines;
+
+ public int CurrentHoverCombo { get; private set; }
+
+ public GraphContainer()
+ {
+ InternalChild = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Colour = OsuColour.Gray(0.1f),
+ RelativeSizeAxes = Axes.Both,
+ },
+ verticalGridLines = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ hoverLine = new Box
+ {
+ Colour = Color4.Yellow,
+ RelativeSizeAxes = Axes.Y,
+ Origin = Anchor.TopCentre,
+ Alpha = 0,
+ Width = 1,
+ },
+ missLines = new Container
+ {
+ Alpha = 0.6f,
+ RelativeSizeAxes = Axes.Both,
+ },
+ Content,
+ }
+ };
+
+ MissLocations.BindCollectionChanged((_, _) => updateMissLocations());
+ NonPerfectLocations.BindCollectionChanged((_, _) => updateMissLocations());
+
+ MaxCombo.BindValueChanged(_ =>
+ {
+ updateMissLocations();
+ updateVerticalGridLines();
+ }, true);
+ }
+
+ private void updateVerticalGridLines()
+ {
+ verticalGridLines.Clear();
+
+ for (int i = 0; i < MaxCombo.Value; i++)
+ {
+ if (i % 100 == 0)
+ {
+ verticalGridLines.AddRange(new Drawable[]
+ {
+ new Box
+ {
+ Colour = OsuColour.Gray(0.2f),
+ Origin = Anchor.TopCentre,
+ Width = 1,
+ RelativeSizeAxes = Axes.Y,
+ RelativePositionAxes = Axes.X,
+ X = (float)i / MaxCombo.Value,
+ },
+ new OsuSpriteText
+ {
+ RelativePositionAxes = Axes.X,
+ X = (float)i / MaxCombo.Value,
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ Text = $"{i:#,0}",
+ Rotation = -30,
+ Y = -20,
+ }
+ });
+ }
+ }
+ }
+
+ private void updateMissLocations()
+ {
+ missLines.Clear();
+
+ foreach (int miss in MissLocations)
+ {
+ missLines.Add(new Box
+ {
+ Colour = Color4.Red,
+ Origin = Anchor.TopCentre,
+ Width = 1,
+ RelativeSizeAxes = Axes.Y,
+ RelativePositionAxes = Axes.X,
+ X = (float)miss / MaxCombo.Value,
+ });
+ }
+
+ foreach (int miss in NonPerfectLocations)
+ {
+ missLines.Add(new Box
+ {
+ Colour = Color4.Orange,
+ Origin = Anchor.TopCentre,
+ Width = 1,
+ RelativeSizeAxes = Axes.Y,
+ RelativePositionAxes = Axes.X,
+ X = (float)miss / MaxCombo.Value,
+ });
+ }
+ }
+
+ protected override bool OnHover(HoverEvent e)
+ {
+ hoverLine.Show();
+ return base.OnHover(e);
+ }
+
+ protected override void OnHoverLost(HoverLostEvent e)
+ {
+ hoverLine.Hide();
+ base.OnHoverLost(e);
+ }
+
+ protected override bool OnMouseMove(MouseMoveEvent e)
+ {
+ CurrentHoverCombo = (int)(e.MousePosition.X / DrawWidth * MaxCombo.Value);
+
+ hoverLine.X = e.MousePosition.X;
+ return base.OnMouseMove(e);
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ if (e.Button == MouseButton.Left)
+ MissLocations.Add(CurrentHoverCombo);
+ else
+ NonPerfectLocations.Add(CurrentHoverCombo);
+
+ return true;
+ }
+
+ private GraphTooltip? tooltip;
+
+ public ITooltip> GetCustomTooltip() => tooltip ??= new GraphTooltip(this);
+
+ public IEnumerable TooltipContent => Content.OfType();
+
+ public class GraphTooltip : CompositeDrawable, ITooltip>
+ {
+ private readonly GraphContainer graphContainer;
+
+ private readonly OsuTextFlowContainer textFlow;
+
+ public GraphTooltip(GraphContainer graphContainer)
+ {
+ this.graphContainer = graphContainer;
+ AutoSizeAxes = Axes.Both;
+
+ Masking = true;
+ CornerRadius = 10;
+
+ InternalChildren = new Drawable[]
+ {
+ new Box
+ {
+ Colour = OsuColour.Gray(0.15f),
+ RelativeSizeAxes = Axes.Both,
+ },
+ textFlow = new OsuTextFlowContainer
+ {
+ Colour = Color4.White,
+ AutoSizeAxes = Axes.Both,
+ Padding = new MarginPadding(10),
+ }
+ };
+ }
+
+ private int? lastContentCombo;
+
+ public void SetContent(IEnumerable content)
+ {
+ int relevantCombo = graphContainer.CurrentHoverCombo;
+
+ if (lastContentCombo == relevantCombo)
+ return;
+
+ lastContentCombo = relevantCombo;
+ textFlow.Clear();
+
+ textFlow.AddParagraph($"At combo {relevantCombo}:");
+
+ foreach (var graph in content)
+ {
+ float valueAtHover = graph.Values.ElementAt(relevantCombo);
+ float ofTotal = valueAtHover / graph.Values.Last();
+
+ textFlow.AddParagraph($"{graph.Name}: {valueAtHover:#,0} ({ofTotal * 100:N0}% of final)\n", st => st.Colour = graph.LineColour);
+ }
+ }
+
+ public void Move(Vector2 pos) => this.MoveTo(pos);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs b/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs
index 4f05194e08..16110e5595 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs
@@ -3,18 +3,17 @@
#nullable disable
-using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Tests.Visual.UserInterface;
namespace osu.Game.Tests.Visual.Settings
{
- public class TestSceneDirectorySelector : OsuTestScene
+ public class TestSceneDirectorySelector : ThemeComparisonTestScene
{
- [BackgroundDependencyLoader]
- private void load()
+ protected override Drawable CreateContent() => new OsuDirectorySelector
{
- Add(new OsuDirectorySelector { RelativeSizeAxes = Axes.Both });
- }
+ RelativeSizeAxes = Axes.Both
+ };
}
}
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs b/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs
index 6f25012bfa..97bf0d212a 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs
@@ -4,23 +4,43 @@
#nullable disable
using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Tests.Visual.UserInterface;
namespace osu.Game.Tests.Visual.Settings
{
- public class TestSceneFileSelector : OsuTestScene
+ public class TestSceneFileSelector : ThemeComparisonTestScene
{
- [Test]
- public void TestAllFiles()
- {
- AddStep("create", () => Child = new OsuFileSelector { RelativeSizeAxes = Axes.Both });
- }
+ [Resolved]
+ private OsuColour colours { get; set; }
[Test]
public void TestJpgFilesOnly()
{
- AddStep("create", () => Child = new OsuFileSelector(validFileExtensions: new[] { ".jpg" }) { RelativeSizeAxes = Axes.Both });
+ AddStep("create", () =>
+ {
+ Cell(0, 0).Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colours.GreySeaFoam
+ },
+ new OsuFileSelector(validFileExtensions: new[] { ".jpg" })
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ };
+ });
}
+
+ protected override Drawable CreateContent() => new OsuFileSelector
+ {
+ RelativeSizeAxes = Axes.Both,
+ };
}
}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
index 0e72463d1e..eacaf7f92e 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
@@ -862,52 +862,6 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1);
}
- [Test]
- public void TestRandomFallbackOnNonMatchingPrevious()
- {
- List manySets = new List();
-
- AddStep("populate maps", () =>
- {
- manySets.Clear();
-
- for (int i = 0; i < 10; i++)
- {
- manySets.Add(TestResources.CreateTestBeatmapSetInfo(3, new[]
- {
- // all taiko except for first
- rulesets.GetRuleset(i > 0 ? 1 : 0)
- }));
- }
- });
-
- loadBeatmaps(manySets);
-
- for (int i = 0; i < 10; i++)
- {
- AddStep("Reset filter", () => carousel.Filter(new FilterCriteria(), false));
-
- AddStep("select first beatmap", () => carousel.SelectBeatmap(manySets.First().Beatmaps.First()));
-
- AddStep("Toggle non-matching filter", () =>
- {
- carousel.Filter(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }, false);
- });
-
- AddAssert("selection lost", () => carousel.SelectedBeatmapInfo == null);
-
- AddStep("Restore different ruleset filter", () =>
- {
- carousel.Filter(new FilterCriteria { Ruleset = rulesets.GetRuleset(1) }, false);
- eagerSelectedIDs.Add(carousel.SelectedBeatmapSet!.ID);
- });
-
- AddAssert("selection changed", () => !carousel.SelectedBeatmapInfo!.Equals(manySets.First().Beatmaps.First()));
- }
-
- AddAssert("Selection was random", () => eagerSelectedIDs.Count > 2);
- }
-
[Test]
public void TestFilteringByUserStarDifficulty()
{
@@ -955,6 +909,63 @@ namespace osu.Game.Tests.Visual.SongSelect
checkVisibleItemCount(true, 15);
}
+ [Test]
+ public void TestCarouselSelectsNextWhenPreviousIsFiltered()
+ {
+ List sets = new List();
+
+ // 10 sets that go osu! -> taiko -> catch -> osu! -> ...
+ for (int i = 0; i < 10; i++)
+ {
+ var rulesetInfo = rulesets.AvailableRulesets.ElementAt(i % 3);
+ sets.Add(TestResources.CreateTestBeatmapSetInfo(5, new[] { rulesetInfo }));
+ }
+
+ // Sort mode is important to keep the ruleset order
+ loadBeatmaps(sets, () => new FilterCriteria { Sort = SortMode.Title });
+ setSelected(1, 1);
+
+ for (int i = 1; i < 10; i++)
+ {
+ var rulesetInfo = rulesets.AvailableRulesets.ElementAt(i % 3);
+ AddStep($"Set ruleset to {rulesetInfo.ShortName}", () =>
+ {
+ carousel.Filter(new FilterCriteria { Ruleset = rulesetInfo, Sort = SortMode.Title }, false);
+ });
+ waitForSelection(i + 1, 1);
+ }
+ }
+
+ [Test]
+ public void TestCarouselSelectsBackwardsWhenDistanceIsShorter()
+ {
+ List sets = new List();
+
+ // 10 sets that go taiko, osu!, osu!, osu!, taiko, osu!, osu!, osu!, ...
+ for (int i = 0; i < 10; i++)
+ {
+ var rulesetInfo = rulesets.AvailableRulesets.ElementAt(i % 4 == 0 ? 1 : 0);
+ sets.Add(TestResources.CreateTestBeatmapSetInfo(5, new[] { rulesetInfo }));
+ }
+
+ // Sort mode is important to keep the ruleset order
+ loadBeatmaps(sets, () => new FilterCriteria { Sort = SortMode.Title });
+
+ for (int i = 2; i < 10; i += 4)
+ {
+ setSelected(i, 1);
+ AddStep("Set ruleset to taiko", () =>
+ {
+ carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(1), Sort = SortMode.Title }, false);
+ });
+ waitForSelection(i - 1, 1);
+ AddStep("Remove ruleset filter", () =>
+ {
+ carousel.Filter(new FilterCriteria { Sort = SortMode.Title }, false);
+ });
+ }
+ }
+
private void loadBeatmaps(List beatmapSets = null, Func initialCriteria = null, Action carouselAdjust = null, int? count = null,
bool randomDifficulties = false)
{
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSafeAreaHandling.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSafeAreaHandling.cs
index 2ba0fa36c3..90365ec939 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneSafeAreaHandling.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSafeAreaHandling.cs
@@ -8,6 +8,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Game.Configuration;
using osu.Game.Overlays.Settings;
using osuTK.Graphics;
@@ -24,6 +25,8 @@ namespace osu.Game.Tests.Visual.UserInterface
private readonly Bindable safeAreaPaddingLeft = new BindableFloat { MinValue = 0, MaxValue = 200 };
private readonly Bindable safeAreaPaddingRight = new BindableFloat { MinValue = 0, MaxValue = 200 };
+ private readonly Bindable applySafeAreaConsiderations = new Bindable(true);
+
protected override void LoadComplete()
{
base.LoadComplete();
@@ -84,6 +87,11 @@ namespace osu.Game.Tests.Visual.UserInterface
Current = safeAreaPaddingRight,
LabelText = "Right"
},
+ new SettingsCheckbox
+ {
+ LabelText = "Apply",
+ Current = applySafeAreaConsiderations,
+ },
}
}
}
@@ -93,6 +101,7 @@ namespace osu.Game.Tests.Visual.UserInterface
safeAreaPaddingBottom.BindValueChanged(_ => updateSafeArea());
safeAreaPaddingLeft.BindValueChanged(_ => updateSafeArea());
safeAreaPaddingRight.BindValueChanged(_ => updateSafeArea());
+ applySafeAreaConsiderations.BindValueChanged(_ => updateSafeArea());
});
base.SetUpSteps();
@@ -107,6 +116,8 @@ namespace osu.Game.Tests.Visual.UserInterface
Left = safeAreaPaddingLeft.Value,
Right = safeAreaPaddingRight.Value,
};
+
+ Game.LocalConfig.SetValue(OsuSetting.SafeAreaConsiderations, applySafeAreaConsiderations.Value);
}
[Test]
diff --git a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs
index d314f40c30..45dffdc94a 100644
--- a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs
+++ b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
[Test]
public void TestCustomDirectory()
{
- using (HeadlessGameHost host = new TestRunHeadlessGameHost(nameof(TestCustomDirectory), null)) // don't use clean run as we are writing a config file.
+ using (HeadlessGameHost host = new TestRunHeadlessGameHost(nameof(TestCustomDirectory))) // don't use clean run as we are writing a config file.
{
string osuDesktopStorage = Path.Combine(host.UserStoragePaths.First(), nameof(TestCustomDirectory));
const string custom_tournament = "custom";
@@ -49,7 +49,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
// manual cleaning so we can prepare a config file.
storage.DeleteDirectory(string.Empty);
- using (var storageConfig = new TournamentStorageManager(storage))
+ using (var storageConfig = new TournamentConfigManager(storage))
storageConfig.SetValue(StorageConfig.CurrentTournament, custom_tournament);
try
@@ -66,82 +66,5 @@ namespace osu.Game.Tournament.Tests.NonVisual
}
}
}
-
- [Test]
- public void TestMigration()
- {
- using (HeadlessGameHost host = new TestRunHeadlessGameHost(nameof(TestMigration), null)) // don't use clean run as we are writing test files for migration.
- {
- string osuRoot = Path.Combine(host.UserStoragePaths.First(), nameof(TestMigration));
- string configFile = Path.Combine(osuRoot, "tournament.ini");
-
- if (File.Exists(configFile))
- File.Delete(configFile);
-
- // Recreate the old setup that uses "tournament" as the base path.
- string oldPath = Path.Combine(osuRoot, "tournament");
-
- string videosPath = Path.Combine(oldPath, "Videos");
- string modsPath = Path.Combine(oldPath, "Mods");
- string flagsPath = Path.Combine(oldPath, "Flags");
-
- Directory.CreateDirectory(videosPath);
- Directory.CreateDirectory(modsPath);
- Directory.CreateDirectory(flagsPath);
-
- // Define testing files corresponding to the specific file migrations that are needed
- string bracketFile = Path.Combine(osuRoot, TournamentGameBase.BRACKET_FILENAME);
-
- string drawingsConfig = Path.Combine(osuRoot, "drawings.ini");
- string drawingsFile = Path.Combine(osuRoot, "drawings.txt");
- string drawingsResult = Path.Combine(osuRoot, "drawings_results.txt");
-
- // Define sample files to test recursive copying
- string videoFile = Path.Combine(videosPath, "video.mp4");
- string modFile = Path.Combine(modsPath, "mod.png");
- string flagFile = Path.Combine(flagsPath, "flag.png");
-
- File.WriteAllText(bracketFile, "{}");
- File.WriteAllText(drawingsConfig, "test");
- File.WriteAllText(drawingsFile, "test");
- File.WriteAllText(drawingsResult, "test");
- File.WriteAllText(videoFile, "test");
- File.WriteAllText(modFile, "test");
- File.WriteAllText(flagFile, "test");
-
- try
- {
- var osu = LoadTournament(host);
-
- var storage = osu.Dependencies.Get();
-
- string migratedPath = Path.Combine(host.Storage.GetFullPath("."), "tournaments", "default");
-
- videosPath = Path.Combine(migratedPath, "Videos");
- modsPath = Path.Combine(migratedPath, "Mods");
- flagsPath = Path.Combine(migratedPath, "Flags");
-
- videoFile = Path.Combine(videosPath, "video.mp4");
- modFile = Path.Combine(modsPath, "mod.png");
- flagFile = Path.Combine(flagsPath, "flag.png");
-
- Assert.That(storage.GetFullPath("."), Is.EqualTo(migratedPath));
-
- Assert.True(storage.Exists(TournamentGameBase.BRACKET_FILENAME));
- Assert.True(storage.Exists("drawings.txt"));
- Assert.True(storage.Exists("drawings_results.txt"));
-
- Assert.True(storage.Exists("drawings.ini"));
-
- Assert.True(storage.Exists(videoFile));
- Assert.True(storage.Exists(modFile));
- Assert.True(storage.Exists(flagFile));
- }
- finally
- {
- host.Exit();
- }
- }
- }
}
}
diff --git a/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs
index 1bbbcc3661..ca6354cb48 100644
--- a/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs
+++ b/osu.Game.Tournament.Tests/NonVisual/IPCLocationTest.cs
@@ -21,7 +21,7 @@ namespace osu.Game.Tournament.Tests.NonVisual
public void CheckIPCLocation()
{
// don't use clean run because files are being written before osu! launches.
- using (var host = new TestRunHeadlessGameHost(nameof(CheckIPCLocation), null))
+ using (var host = new TestRunHeadlessGameHost(nameof(CheckIPCLocation)))
{
string basePath = Path.Combine(host.UserStoragePaths.First(), nameof(CheckIPCLocation));
diff --git a/osu.Game.Tournament/Configuration/TournamentStorageManager.cs b/osu.Game.Tournament/Configuration/TournamentConfigManager.cs
similarity index 55%
rename from osu.Game.Tournament/Configuration/TournamentStorageManager.cs
rename to osu.Game.Tournament/Configuration/TournamentConfigManager.cs
index 0b9a556296..8f256ba9c3 100644
--- a/osu.Game.Tournament/Configuration/TournamentStorageManager.cs
+++ b/osu.Game.Tournament/Configuration/TournamentConfigManager.cs
@@ -8,14 +8,23 @@ using osu.Framework.Platform;
namespace osu.Game.Tournament.Configuration
{
- public class TournamentStorageManager : IniConfigManager
+ public class TournamentConfigManager : IniConfigManager
{
protected override string Filename => "tournament.ini";
- public TournamentStorageManager(Storage storage)
+ private const string default_tournament = "default";
+
+ public TournamentConfigManager(Storage storage)
: base(storage)
{
}
+
+ protected override void InitialiseDefaults()
+ {
+ base.InitialiseDefaults();
+
+ SetDefault(StorageConfig.CurrentTournament, default_tournament);
+ }
}
public enum StorageConfig
diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs
index bd52b6dfed..e59f90a45e 100644
--- a/osu.Game.Tournament/IO/TournamentStorage.cs
+++ b/osu.Game.Tournament/IO/TournamentStorage.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.Collections.Generic;
-using System.IO;
using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Framework.Platform;
@@ -13,35 +10,28 @@ using osu.Game.Tournament.Configuration;
namespace osu.Game.Tournament.IO
{
- public class TournamentStorage : MigratableStorage
+ public class TournamentStorage : WrappedStorage
{
- private const string default_tournament = "default";
- private readonly Storage storage;
-
///
/// The storage where all tournaments are located.
///
public readonly Storage AllTournaments;
- private readonly TournamentStorageManager storageConfig;
public readonly Bindable CurrentTournament;
+ protected TournamentConfigManager TournamentConfigManager { get; }
+
public TournamentStorage(Storage storage)
: base(storage.GetStorageForDirectory("tournaments"), string.Empty)
{
- this.storage = storage;
AllTournaments = UnderlyingStorage;
- storageConfig = new TournamentStorageManager(storage);
+ TournamentConfigManager = new TournamentConfigManager(storage);
- if (storage.Exists("tournament.ini"))
- {
- ChangeTargetStorage(AllTournaments.GetStorageForDirectory(storageConfig.Get(StorageConfig.CurrentTournament)));
- }
- else
- Migrate(AllTournaments.GetStorageForDirectory(default_tournament));
+ CurrentTournament = TournamentConfigManager.GetBindable(StorageConfig.CurrentTournament);
+
+ ChangeTargetStorage(AllTournaments.GetStorageForDirectory(CurrentTournament.Value));
- CurrentTournament = storageConfig.GetBindable(StorageConfig.CurrentTournament);
Logger.Log("Using tournament storage: " + GetFullPath(string.Empty));
CurrentTournament.BindValueChanged(updateTournament);
@@ -53,62 +43,6 @@ namespace osu.Game.Tournament.IO
Logger.Log("Changing tournament storage: " + GetFullPath(string.Empty));
}
- protected override void ChangeTargetStorage(Storage newStorage)
- {
- // due to an unfortunate oversight, on OSes that are sensitive to pathname casing
- // the custom flags directory needed to be named `Flags` (uppercase),
- // while custom mods and videos directories needed to be named `mods` and `videos` respectively (lowercase).
- // to unify handling to uppercase, move any non-compliant directories automatically for the user to migrate.
- // can be removed 20220528
- if (newStorage.ExistsDirectory("flags"))
- AttemptOperation(() => Directory.Move(newStorage.GetFullPath("flags"), newStorage.GetFullPath("Flags")));
- if (newStorage.ExistsDirectory("mods"))
- AttemptOperation(() => Directory.Move(newStorage.GetFullPath("mods"), newStorage.GetFullPath("Mods")));
- if (newStorage.ExistsDirectory("videos"))
- AttemptOperation(() => Directory.Move(newStorage.GetFullPath("videos"), newStorage.GetFullPath("Videos")));
-
- base.ChangeTargetStorage(newStorage);
- }
-
public IEnumerable ListTournaments() => AllTournaments.GetDirectories(string.Empty);
-
- public override bool Migrate(Storage newStorage)
- {
- // this migration only happens once on moving to the per-tournament storage system.
- // listed files are those known at that point in time.
- // this can be removed at some point in the future (6 months obsoletion would mean 2021-04-19)
-
- var source = new DirectoryInfo(storage.GetFullPath("tournament"));
- var destination = new DirectoryInfo(newStorage.GetFullPath("."));
-
- if (source.Exists)
- {
- Logger.Log("Migrating tournament assets to default tournament storage.");
- CopyRecursive(source, destination);
- DeleteRecursive(source);
- }
-
- moveFileIfExists(TournamentGameBase.BRACKET_FILENAME, destination);
- moveFileIfExists("drawings.txt", destination);
- moveFileIfExists("drawings_results.txt", destination);
- moveFileIfExists("drawings.ini", destination);
-
- ChangeTargetStorage(newStorage);
- storageConfig.SetValue(StorageConfig.CurrentTournament, default_tournament);
- storageConfig.Save();
-
- return true;
- }
-
- private void moveFileIfExists(string file, DirectoryInfo destination)
- {
- if (!storage.Exists(file))
- return;
-
- Logger.Log($"Migrating {file} to default tournament storage.");
- var fileInfo = new System.IO.FileInfo(storage.GetFullPath(file));
- AttemptOperation(() => fileInfo.CopyTo(Path.Combine(destination.FullName, fileInfo.Name), true));
- fileInfo.Delete();
- }
}
}
diff --git a/osu.Game.Tournament/JsonPointConverter.cs b/osu.Game.Tournament/JsonPointConverter.cs
index db48c36c99..d3b40a3526 100644
--- a/osu.Game.Tournament/JsonPointConverter.cs
+++ b/osu.Game.Tournament/JsonPointConverter.cs
@@ -6,6 +6,7 @@
using System;
using System.Diagnostics;
using System.Drawing;
+using System.Globalization;
using Newtonsoft.Json;
namespace osu.Game.Tournament
@@ -31,7 +32,9 @@ namespace osu.Game.Tournament
Debug.Assert(str != null);
- return new PointConverter().ConvertFromString(str) as Point? ?? new Point();
+ // Null check suppression is required due to .NET standard expecting a non-null context.
+ // Seems to work fine at a runtime level (and the parameter is nullable in .NET 6+).
+ return new PointConverter().ConvertFromString(null!, CultureInfo.InvariantCulture, str) as Point? ?? new Point();
}
var point = new Point();
diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
index 1bc929604d..348661e2a3 100644
--- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
+++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs
@@ -239,17 +239,17 @@ namespace osu.Game.Tournament.Screens.Editors
var req = new GetBeatmapRequest(new APIBeatmap { OnlineID = Model.ID });
- req.Success += res =>
+ req.Success += res => Schedule(() =>
{
Model.Beatmap = new TournamentBeatmap(res);
updatePanel();
- };
+ });
- req.Failure += _ =>
+ req.Failure += _ => Schedule(() =>
{
Model.Beatmap = null;
updatePanel();
- };
+ });
API.Queue(req);
}, true);
diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs
index 4419791e43..c7c244bf0e 100644
--- a/osu.Game/Beatmaps/BeatmapConverter.cs
+++ b/osu.Game/Beatmaps/BeatmapConverter.cs
@@ -47,6 +47,7 @@ namespace osu.Game.Beatmaps
// Shallow clone isn't enough to ensure we don't mutate beatmap info unexpectedly.
// Can potentially be removed after `Beatmap.Difficulty` doesn't save back to `Beatmap.BeatmapInfo`.
original.BeatmapInfo = original.BeatmapInfo.Clone();
+ original.ControlPointInfo = original.ControlPointInfo.DeepClone();
return ConvertBeatmap(original, cancellationToken);
}
diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs
index 6f9df1ba7f..3208598f56 100644
--- a/osu.Game/Beatmaps/BeatmapInfo.cs
+++ b/osu.Game/Beatmaps/BeatmapInfo.cs
@@ -238,14 +238,6 @@ namespace osu.Game.Beatmaps
#region Compatibility properties
- [Ignored]
- [Obsolete("Use BeatmapInfo.Difficulty instead.")] // can be removed 20220719
- public BeatmapDifficulty BaseDifficulty
- {
- get => Difficulty;
- set => Difficulty = value;
- }
-
[Ignored]
public string? Path => File?.Filename;
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index befc56d244..965cc43815 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -340,7 +340,7 @@ namespace osu.Game.Beatmaps
static string createBeatmapFilenameFromMetadata(BeatmapInfo beatmapInfo)
{
var metadata = beatmapInfo.Metadata;
- return $"{metadata.Artist} - {metadata.Title} ({metadata.Author.Username}) [{beatmapInfo.DifficultyName}].osu".GetValidArchiveContentFilename();
+ return $"{metadata.Artist} - {metadata.Title} ({metadata.Author.Username}) [{beatmapInfo.DifficultyName}].osu".GetValidFilename();
}
}
diff --git a/osu.Game/Beatmaps/Formats/IHasComboColours.cs b/osu.Game/Beatmaps/Formats/IHasComboColours.cs
index d5e96da246..1d9cc0be65 100644
--- a/osu.Game/Beatmaps/Formats/IHasComboColours.cs
+++ b/osu.Game/Beatmaps/Formats/IHasComboColours.cs
@@ -3,7 +3,6 @@
#nullable disable
-using System;
using System.Collections.Generic;
using osuTK.Graphics;
@@ -22,11 +21,5 @@ namespace osu.Game.Beatmaps.Formats
/// if empty, will fall back to default combo colours.
///
List CustomComboColours { get; }
-
- ///
- /// Adds combo colours to the list.
- ///
- [Obsolete("Use CustomComboColours directly.")] // can be removed 20220215
- void AddComboColours(params Color4[] colours);
}
}
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
index 5f5749dc73..5f0a2a0824 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -435,8 +435,10 @@ namespace osu.Game.Beatmaps.Formats
addControlPoint(time, controlPoint, true);
}
+ int onlineRulesetID = beatmap.BeatmapInfo.Ruleset.OnlineID;
+
#pragma warning disable 618
- addControlPoint(time, new LegacyDifficultyControlPoint(beatLength)
+ addControlPoint(time, new LegacyDifficultyControlPoint(onlineRulesetID, beatLength)
#pragma warning restore 618
{
SliderVelocity = speedMultiplier,
@@ -448,8 +450,6 @@ namespace osu.Game.Beatmaps.Formats
OmitFirstBarLine = omitFirstBarSignature,
};
- int onlineRulesetID = beatmap.BeatmapInfo.Ruleset.OnlineID;
-
// osu!taiko and osu!mania use effect points rather than difficulty points for scroll speed adjustments.
if (onlineRulesetID == 1 || onlineRulesetID == 3)
effectPoint.ScrollSpeed = speedMultiplier;
diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index 9c066ada08..ed7ca47cfd 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -174,11 +174,15 @@ namespace osu.Game.Beatmaps.Formats
///
public bool GenerateTicks { get; private set; } = true;
- public LegacyDifficultyControlPoint(double beatLength)
+ public LegacyDifficultyControlPoint(int rulesetId, double beatLength)
: this()
{
// Note: In stable, the division occurs on floats, but with compiler optimisations turned on actually seems to occur on doubles via some .NET black magic (possibly inlining?).
- BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100.0 : 1;
+ if (rulesetId == 1 || rulesetId == 3)
+ BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100.0 : 1;
+ else
+ BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 1000) / 100.0 : 1;
+
GenerateTicks = !double.IsNaN(beatLength);
}
diff --git a/osu.Game/Beatmaps/Timing/TimeSignatures.cs b/osu.Game/Beatmaps/Timing/TimeSignatures.cs
deleted file mode 100644
index 95c971eebf..0000000000
--- a/osu.Game/Beatmaps/Timing/TimeSignatures.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-// 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 System.ComponentModel;
-
-namespace osu.Game.Beatmaps.Timing
-{
- [Obsolete("Use osu.Game.Beatmaps.Timing.TimeSignature instead.")]
- public enum TimeSignatures // can be removed 20220722
- {
- [Description("4/4")]
- SimpleQuadruple = 4,
-
- [Description("3/4")]
- SimpleTriple = 3
- }
-}
diff --git a/osu.Game/Configuration/DatabasedSetting.cs b/osu.Game/Configuration/DatabasedSetting.cs
deleted file mode 100644
index 0c1b4021a1..0000000000
--- a/osu.Game/Configuration/DatabasedSetting.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-// 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.ComponentModel.DataAnnotations.Schema;
-using osu.Game.Database;
-
-namespace osu.Game.Configuration
-{
- [Table("Settings")]
- public class DatabasedSetting : IHasPrimaryKey // can be removed 20220315.
- {
- public int ID { get; set; }
-
- public bool IsManaged => ID > 0;
-
- public int? RulesetID { get; set; }
-
- public int? Variant { get; set; }
-
- public int? SkinInfoID { get; set; }
-
- [Column("Key")]
- public string Key { get; set; }
-
- [Column("Value")]
- public string StringValue
- {
- get => Value.ToString();
- set => Value = value;
- }
-
- public object Value;
-
- public DatabasedSetting(string key, object value)
- {
- Key = key;
- Value = value;
- }
-
- ///
- /// Constructor for derived classes that may require serialisation.
- ///
- public DatabasedSetting()
- {
- }
-
- public override string ToString() => $"{Key}=>{Value}";
- }
-}
diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs
index 1378e1691a..093eaa0f31 100644
--- a/osu.Game/Configuration/OsuConfigManager.cs
+++ b/osu.Game/Configuration/OsuConfigManager.cs
@@ -118,7 +118,6 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.Prefer24HourTime, CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern.Contains(@"tt"));
// Gameplay
- SetDefault(OsuSetting.PositionalHitsounds, true); // replaced by level setting below, can be removed 20220703.
SetDefault(OsuSetting.PositionalHitsoundsLevel, 0.2f, 0, 1);
SetDefault(OsuSetting.DimLevel, 0.7, 0, 1, 0.01);
SetDefault(OsuSetting.BlurLevel, 0, 0, 1, 0.01);
@@ -127,7 +126,6 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.HitLighting, true);
SetDefault(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always);
- SetDefault(OsuSetting.ShowProgressGraph, true);
SetDefault(OsuSetting.ShowHealthDisplayWhenCantFail, true);
SetDefault(OsuSetting.FadePlayfieldWhenHealthLow, true);
SetDefault(OsuSetting.KeyOverlay, false);
@@ -154,6 +152,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.SongSelectRightMouseScroll, false);
SetDefault(OsuSetting.Scaling, ScalingMode.Off);
+ SetDefault(OsuSetting.SafeAreaConsiderations, true);
SetDefault(OsuSetting.ScalingSizeX, 0.8f, 0.2f, 1f);
SetDefault(OsuSetting.ScalingSizeY, 0.8f, 0.2f, 1f);
@@ -203,14 +202,11 @@ namespace osu.Game.Configuration
if (!int.TryParse(pieces[0], out int year)) return;
if (!int.TryParse(pieces[1], out int monthDay)) return;
+ // ReSharper disable once UnusedVariable
int combined = (year * 10000) + monthDay;
- if (combined < 20220103)
- {
- var positionalHitsoundsEnabled = GetBindable(OsuSetting.PositionalHitsounds);
- if (!positionalHitsoundsEnabled.Value)
- SetValue(OsuSetting.PositionalHitsoundsLevel, 0);
- }
+ // migrations can be added here using a condition like:
+ // if (combined < 20220103) { performMigration() }
}
public override TrackedSettings CreateTrackedSettings()
@@ -296,14 +292,11 @@ namespace osu.Game.Configuration
ShowStoryboard,
KeyOverlay,
GameplayLeaderboard,
- PositionalHitsounds,
PositionalHitsoundsLevel,
AlwaysPlayFirstComboBreak,
FloatingComments,
HUDVisibilityMode,
- // This has been migrated to the component itself. can be removed 20221027.
- ShowProgressGraph,
ShowHealthDisplayWhenCantFail,
FadePlayfieldWhenHealthLow,
MouseDisableButtons,
@@ -370,6 +363,7 @@ namespace osu.Game.Configuration
DiscordRichPresence,
AutomaticallyDownloadWhenSpectating,
ShowOnlineExplicitContent,
- LastProcessedMetadataId
+ LastProcessedMetadataId,
+ SafeAreaConsiderations,
}
}
diff --git a/osu.Game/Database/LegacyExporter.cs b/osu.Game/Database/LegacyExporter.cs
index d9fdc40abc..16d7441dde 100644
--- a/osu.Game/Database/LegacyExporter.cs
+++ b/osu.Game/Database/LegacyExporter.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Database
/// The item to export.
public void Export(TModel item)
{
- string filename = $"{item.GetDisplayString().GetValidArchiveContentFilename()}{FileExtension}";
+ string filename = $"{item.GetDisplayString().GetValidFilename()}{FileExtension}";
using (var stream = exportStorage.CreateFileSafely(filename))
ExportModelTo(item, stream);
diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs
index edcd020226..1a938c12e5 100644
--- a/osu.Game/Database/RealmAccess.cs
+++ b/osu.Game/Database/RealmAccess.cs
@@ -857,17 +857,7 @@ namespace osu.Game.Database
if (legacyCollectionImporter.GetAvailableCount(storage).GetResultSafely() > 0)
{
- legacyCollectionImporter.ImportFromStorage(storage).ContinueWith(task =>
- {
- if (task.Exception != null)
- {
- // can be removed 20221027 (just for initial safety).
- Logger.Error(task.Exception.InnerException, "Collections could not be migrated to realm. Please provide your \"collection.db\" to the dev team.");
- return;
- }
-
- storage.Move("collection.db", "collection.db.migrated");
- });
+ legacyCollectionImporter.ImportFromStorage(storage).ContinueWith(_ => storage.Move("collection.db", "collection.db.migrated"));
}
break;
diff --git a/osu.Game/Extensions/ModelExtensions.cs b/osu.Game/Extensions/ModelExtensions.cs
index b10071bb45..efb3c4d633 100644
--- a/osu.Game/Extensions/ModelExtensions.cs
+++ b/osu.Game/Extensions/ModelExtensions.cs
@@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.IO;
-using System.Linq;
+using System.Text.RegularExpressions;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.IO;
@@ -15,6 +15,8 @@ namespace osu.Game.Extensions
{
public static class ModelExtensions
{
+ private static readonly Regex invalid_filename_chars = new Regex(@"(?!$)[^A-Za-z0-9_()[\]. \-]", RegexOptions.Compiled);
+
///
/// Get the relative path in osu! storage for this file.
///
@@ -137,20 +139,14 @@ namespace osu.Game.Extensions
return instance.OnlineID.Equals(other.OnlineID);
}
- private static readonly char[] invalid_filename_characters = Path.GetInvalidFileNameChars()
- // Backslash is added to avoid issues when exporting to zip.
- // See SharpCompress filename normalisation https://github.com/adamhathcock/sharpcompress/blob/a1e7c0068db814c9aa78d86a94ccd1c761af74bd/src/SharpCompress/Writers/Zip/ZipWriter.cs#L143.
- .Append('\\')
- .ToArray();
-
///
- /// Get a valid filename for use inside a zip file. Avoids backslashes being incorrectly converted to directories.
+ /// Create a valid filename which should work across all platforms.
///
- public static string GetValidArchiveContentFilename(this string filename)
- {
- foreach (char c in invalid_filename_characters)
- filename = filename.Replace(c, '_');
- return filename;
- }
+ ///
+ /// This function replaces all characters not included in a very pessimistic list which should be compatible
+ /// across all operating systems. We are using this in place of as
+ /// that function does not have per-platform considerations (and is only made to work on windows).
+ ///
+ public static string GetValidFilename(this string filename) => invalid_filename_chars.Replace(filename, "_");
}
}
diff --git a/osu.Game/Graphics/Containers/ScalingContainer.cs b/osu.Game/Graphics/Containers/ScalingContainer.cs
index 17c51129a7..11e57d4be3 100644
--- a/osu.Game/Graphics/Containers/ScalingContainer.cs
+++ b/osu.Game/Graphics/Containers/ScalingContainer.cs
@@ -29,6 +29,7 @@ namespace osu.Game.Graphics.Containers
private Bindable sizeY;
private Bindable posX;
private Bindable posY;
+ private Bindable applySafeAreaPadding;
private Bindable safeAreaPadding;
@@ -132,6 +133,9 @@ namespace osu.Game.Graphics.Containers
posY = config.GetBindable(OsuSetting.ScalingPositionY);
posY.ValueChanged += _ => Scheduler.AddOnce(updateSize);
+ applySafeAreaPadding = config.GetBindable(OsuSetting.SafeAreaConsiderations);
+ applySafeAreaPadding.BindValueChanged(_ => Scheduler.AddOnce(updateSize));
+
safeAreaPadding = safeArea.SafeAreaPadding.GetBoundCopy();
safeAreaPadding.BindValueChanged(_ => Scheduler.AddOnce(updateSize));
}
@@ -192,7 +196,7 @@ namespace osu.Game.Graphics.Containers
bool requiresMasking = targetRect.Size != Vector2.One
// For the top level scaling container, for now we apply masking if safe areas are in use.
// In the future this can likely be removed as more of the actual UI supports overflowing into the safe areas.
- || (targetMode == ScalingMode.Everything && safeAreaPadding.Value.Total != Vector2.Zero);
+ || (targetMode == ScalingMode.Everything && (applySafeAreaPadding.Value && safeAreaPadding.Value.Total != Vector2.Zero));
if (requiresMasking)
sizableContainer.Masking = true;
@@ -225,6 +229,9 @@ namespace osu.Game.Graphics.Containers
[Resolved]
private ISafeArea safeArea { get; set; }
+ [Resolved]
+ private OsuConfigManager config { get; set; }
+
private readonly bool confineHostCursor;
private readonly LayoutValue cursorRectCache = new LayoutValue(Invalidation.RequiredParentSizeToFit);
@@ -259,7 +266,7 @@ namespace osu.Game.Graphics.Containers
{
if (host.Window == null) return;
- bool coversWholeScreen = Size == Vector2.One && safeArea.SafeAreaPadding.Value.Total == Vector2.Zero;
+ bool coversWholeScreen = Size == Vector2.One && (!config.Get(OsuSetting.SafeAreaConsiderations) || safeArea.SafeAreaPadding.Value.Total == Vector2.Zero);
host.Window.CursorConfineRect = coversWholeScreen ? null : ToScreenSpace(DrawRectangle).AABBFloat;
}
}
diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs
index bbd8f8ecea..8772c1e2d9 100644
--- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs
+++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs
@@ -26,24 +26,24 @@ namespace osu.Game.Graphics.UserInterface
{
set
{
- if (labelText != null)
- labelText.Text = value;
+ if (LabelTextFlowContainer != null)
+ LabelTextFlowContainer.Text = value;
}
}
public MarginPadding LabelPadding
{
- get => labelText?.Padding ?? new MarginPadding();
+ get => LabelTextFlowContainer?.Padding ?? new MarginPadding();
set
{
- if (labelText != null)
- labelText.Padding = value;
+ if (LabelTextFlowContainer != null)
+ LabelTextFlowContainer.Padding = value;
}
}
protected readonly Nub Nub;
- private readonly OsuTextFlowContainer labelText;
+ protected readonly OsuTextFlowContainer LabelTextFlowContainer;
private Sample sampleChecked;
private Sample sampleUnchecked;
@@ -56,7 +56,7 @@ namespace osu.Game.Graphics.UserInterface
Children = new Drawable[]
{
- labelText = new OsuTextFlowContainer(ApplyLabelParameters)
+ LabelTextFlowContainer = new OsuTextFlowContainer(ApplyLabelParameters)
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
@@ -70,19 +70,19 @@ namespace osu.Game.Graphics.UserInterface
Nub.Anchor = Anchor.CentreRight;
Nub.Origin = Anchor.CentreRight;
Nub.Margin = new MarginPadding { Right = nub_padding };
- labelText.Padding = new MarginPadding { Right = Nub.EXPANDED_SIZE + nub_padding * 2 };
+ LabelTextFlowContainer.Padding = new MarginPadding { Right = Nub.EXPANDED_SIZE + nub_padding * 2 };
}
else
{
Nub.Anchor = Anchor.CentreLeft;
Nub.Origin = Anchor.CentreLeft;
Nub.Margin = new MarginPadding { Left = nub_padding };
- labelText.Padding = new MarginPadding { Left = Nub.EXPANDED_SIZE + nub_padding * 2 };
+ LabelTextFlowContainer.Padding = new MarginPadding { Left = Nub.EXPANDED_SIZE + nub_padding * 2 };
}
Nub.Current.BindTo(Current);
- Current.DisabledChanged += disabled => labelText.Alpha = Nub.Alpha = disabled ? 0.3f : 1;
+ Current.DisabledChanged += disabled => LabelTextFlowContainer.Alpha = Nub.Alpha = disabled ? 0.3f : 1;
}
///
diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs
index 42e1073baf..0e348108aa 100644
--- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelector.cs
@@ -31,6 +31,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
protected override DirectorySelectorBreadcrumbDisplay CreateBreadcrumb() => new OsuDirectorySelectorBreadcrumbDisplay();
+ protected override Drawable CreateHiddenToggleButton() => new OsuDirectorySelectorHiddenToggle { Current = { BindTarget = ShowHiddenItems } };
+
protected override DirectorySelectorDirectory CreateParentDirectoryItem(DirectoryInfo directory) => new OsuDirectorySelectorParentDirectory(directory);
protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null) => new OsuDirectorySelectorDirectory(directory, displayName);
diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs
index 0833c7eb8b..08a569269e 100644
--- a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorBreadcrumbDisplay.cs
@@ -25,10 +25,9 @@ namespace osu.Game.Graphics.UserInterfaceV2
protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null) => new OsuBreadcrumbDisplayDirectory(directory, displayName);
- [BackgroundDependencyLoader]
- private void load()
+ public OsuDirectorySelectorBreadcrumbDisplay()
{
- Height = 50;
+ Padding = new MarginPadding(15);
}
private class OsuBreadcrumbDisplayComputer : OsuBreadcrumbDisplayDirectory
diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.cs
new file mode 100644
index 0000000000..7aaf12ca34
--- /dev/null
+++ b/osu.Game/Graphics/UserInterfaceV2/OsuDirectorySelectorHiddenToggle.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 osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Graphics.UserInterfaceV2
+{
+ internal class OsuDirectorySelectorHiddenToggle : OsuCheckbox
+ {
+ public OsuDirectorySelectorHiddenToggle()
+ {
+ RelativeSizeAxes = Axes.None;
+ AutoSizeAxes = Axes.None;
+ Size = new Vector2(100, 50);
+ Anchor = Anchor.CentreLeft;
+ Origin = Anchor.CentreLeft;
+ LabelTextFlowContainer.Anchor = Anchor.CentreLeft;
+ LabelTextFlowContainer.Origin = Anchor.CentreLeft;
+ LabelText = @"Show hidden";
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load(OverlayColourProvider? overlayColourProvider, OsuColour colours)
+ {
+ if (overlayColourProvider != null)
+ return;
+
+ Nub.AccentColour = colours.GreySeaFoamLighter;
+ Nub.GlowingAccentColour = Color4.White;
+ Nub.GlowColour = Color4.White;
+ }
+ }
+}
diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs
index 3e8b7dc209..70af68d595 100644
--- a/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/OsuFileSelector.cs
@@ -33,6 +33,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
protected override DirectorySelectorBreadcrumbDisplay CreateBreadcrumb() => new OsuDirectorySelectorBreadcrumbDisplay();
+ protected override Drawable CreateHiddenToggleButton() => new OsuDirectorySelectorHiddenToggle { Current = { BindTarget = ShowHiddenItems } };
+
protected override DirectorySelectorDirectory CreateParentDirectoryItem(DirectoryInfo directory) => new OsuDirectorySelectorParentDirectory(directory);
protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string displayName = null) => new OsuDirectorySelectorDirectory(directory, displayName);
diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs
index ad53f6d90f..a58c6723ef 100644
--- a/osu.Game/Input/Bindings/GlobalActionContainer.cs
+++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs
@@ -31,14 +31,17 @@ namespace osu.Game.Input.Bindings
parentInputManager = GetContainingInputManager();
}
- // IMPORTANT: Do not change the order of key bindings in this list.
- // It is used to decide the order of precedence (see note in DatabasedKeyBindingContainer).
+ // IMPORTANT: Take care when changing order of the items in the enumerable.
+ // It is used to decide the order of precedence, with the earlier items having higher precedence.
public override IEnumerable DefaultKeyBindings => GlobalKeyBindings
- .Concat(OverlayKeyBindings)
.Concat(EditorKeyBindings)
.Concat(InGameKeyBindings)
.Concat(SongSelectKeyBindings)
- .Concat(AudioControlKeyBindings);
+ .Concat(AudioControlKeyBindings)
+ // Overlay bindings may conflict with more local cases like the editor so they are checked last.
+ // It has generally been agreed on that local screens like the editor should have priority,
+ // based on such usages potentially requiring a lot more key bindings that may be "shared" with global ones.
+ .Concat(OverlayKeyBindings);
public IEnumerable GlobalKeyBindings => new[]
{
@@ -87,6 +90,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.F3 }, GlobalAction.EditorTimingMode),
new KeyBinding(new[] { InputKey.F4 }, GlobalAction.EditorSetupMode),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.A }, GlobalAction.EditorVerifyMode),
+ new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.EditorCloneSelection),
new KeyBinding(new[] { InputKey.J }, GlobalAction.EditorNudgeLeft),
new KeyBinding(new[] { InputKey.K }, GlobalAction.EditorNudgeRight),
new KeyBinding(new[] { InputKey.G }, GlobalAction.EditorCycleGridDisplayMode),
@@ -343,5 +347,8 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleProfile))]
ToggleProfile,
+
+ [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCloneSelection))]
+ EditorCloneSelection
}
}
diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs
index 172818c1c0..14e0bbbced 100644
--- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs
+++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs
@@ -184,6 +184,11 @@ namespace osu.Game.Localisation
///
public static LocalisableString EditorTapForBPM => new TranslatableString(getKey(@"editor_tap_for_bpm"), @"Tap for BPM");
+ ///
+ /// "Clone selection"
+ ///
+ public static LocalisableString EditorCloneSelection => new TranslatableString(getKey(@"editor_clone_selection"), @"Clone selection");
+
///
/// "Cycle grid display mode"
///
diff --git a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs
index 4469d50acb..77dcfd39e3 100644
--- a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs
+++ b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs
@@ -44,7 +44,8 @@ namespace osu.Game.Online.API.Requests.Responses
public int MaxCombo { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
- [JsonProperty("rank")]
+ // ScoreRank is aligned to make 0 equal D. We still want to serialise this (even when DefaultValueHandling.Ignore is used).
+ [JsonProperty("rank", DefaultValueHandling = DefaultValueHandling.Include)]
public ScoreRank Rank { get; set; }
[JsonProperty("started_at")]
@@ -153,10 +154,8 @@ namespace osu.Game.Online.API.Requests.Responses
var mods = Mods.Select(apiMod => apiMod.ToMod(rulesetInstance)).ToArray();
- var scoreInfo = ToScoreInfo(mods);
-
+ var scoreInfo = ToScoreInfo(mods, beatmap);
scoreInfo.Ruleset = ruleset;
- if (beatmap != null) scoreInfo.BeatmapInfo = beatmap;
return scoreInfo;
}
@@ -165,25 +164,47 @@ namespace osu.Game.Online.API.Requests.Responses
/// Create a from an API score instance.
///
/// The mod instances, resolved from a ruleset.
- ///
- public ScoreInfo ToScoreInfo(Mod[] mods) => new ScoreInfo
+ /// The object to populate the scores' beatmap with.
+ ///
+ /// - If this is a type, then the score will be fully populated with the given object.
+ /// - Otherwise, if this is an type (e.g. ), then only the beatmap ruleset will be populated.
+ /// - Otherwise, if this is null, then the beatmap ruleset will not be populated.
+ /// - The online beatmap ID is populated in all cases.
+ ///
+ ///
+ /// The populated .
+ public ScoreInfo ToScoreInfo(Mod[] mods, IBeatmapInfo? beatmap = null)
{
- OnlineID = OnlineID,
- User = User ?? new APIUser { Id = UserID },
- BeatmapInfo = new BeatmapInfo { OnlineID = BeatmapID },
- Ruleset = new RulesetInfo { OnlineID = RulesetID },
- Passed = Passed,
- TotalScore = TotalScore,
- Accuracy = Accuracy,
- MaxCombo = MaxCombo,
- Rank = Rank,
- Statistics = Statistics,
- MaximumStatistics = MaximumStatistics,
- Date = EndedAt,
- Hash = HasReplay ? "online" : string.Empty, // TODO: temporary?
- Mods = mods,
- PP = PP,
- };
+ var score = new ScoreInfo
+ {
+ OnlineID = OnlineID,
+ User = User ?? new APIUser { Id = UserID },
+ BeatmapInfo = new BeatmapInfo { OnlineID = BeatmapID },
+ Ruleset = new RulesetInfo { OnlineID = RulesetID },
+ Passed = Passed,
+ TotalScore = TotalScore,
+ Accuracy = Accuracy,
+ MaxCombo = MaxCombo,
+ Rank = Rank,
+ Statistics = Statistics,
+ MaximumStatistics = MaximumStatistics,
+ Date = EndedAt,
+ Hash = HasReplay ? "online" : string.Empty, // TODO: temporary?
+ Mods = mods,
+ PP = PP,
+ };
+
+ if (beatmap is BeatmapInfo realmBeatmap)
+ score.BeatmapInfo = realmBeatmap;
+ else if (beatmap != null)
+ {
+ score.BeatmapInfo.Ruleset.OnlineID = beatmap.Ruleset.OnlineID;
+ score.BeatmapInfo.Ruleset.Name = beatmap.Ruleset.Name;
+ score.BeatmapInfo.Ruleset.ShortName = beatmap.Ruleset.ShortName;
+ }
+
+ return score;
+ }
///
/// Creates a from a local score for score submission.
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/OsuGame.cs b/osu.Game/OsuGame.cs
index 2bdcb57f2a..4f8098136f 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -179,6 +179,8 @@ namespace osu.Game
private Bindable configRuleset;
+ private Bindable applySafeAreaConsiderations;
+
private Bindable uiScale;
private Bindable configSkin;
@@ -280,10 +282,7 @@ namespace osu.Game
configRuleset = LocalConfig.GetBindable(OsuSetting.Ruleset);
uiScale = LocalConfig.GetBindable(OsuSetting.UIScale);
- var preferredRuleset = int.TryParse(configRuleset.Value, out int rulesetId)
- // int parsing can be removed 20220522
- ? RulesetStore.GetRuleset(rulesetId)
- : RulesetStore.GetRuleset(configRuleset.Value);
+ var preferredRuleset = RulesetStore.GetRuleset(configRuleset.Value);
try
{
@@ -312,6 +311,9 @@ namespace osu.Game
SelectedMods.BindValueChanged(modsChanged);
Beatmap.BindValueChanged(beatmapChanged, true);
+
+ applySafeAreaConsiderations = LocalConfig.GetBindable(OsuSetting.SafeAreaConsiderations);
+ applySafeAreaConsiderations.BindValueChanged(apply => SafeAreaContainer.SafeAreaOverrideEdges = apply.NewValue ? SafeAreaOverrideEdges : Edges.All, true);
}
private ExternalLinkOpener externalLinkOpener;
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 7d9ed7bf3e..39ddffd2d0 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -21,7 +21,11 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input;
using osu.Framework.Input.Handlers;
+using osu.Framework.Input.Handlers.Joystick;
using osu.Framework.Input.Handlers.Midi;
+using osu.Framework.Input.Handlers.Mouse;
+using osu.Framework.Input.Handlers.Tablet;
+using osu.Framework.Input.Handlers.Touch;
using osu.Framework.IO.Stores;
using osu.Framework.Logging;
using osu.Framework.Platform;
@@ -46,6 +50,7 @@ using osu.Game.Online.Spectator;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections;
+using osu.Game.Overlays.Settings.Sections.Input;
using osu.Game.Resources;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
@@ -189,6 +194,8 @@ namespace osu.Game
private RealmAccess realm;
+ protected SafeAreaContainer SafeAreaContainer { get; private set; }
+
///
/// For now, this is used as a source specifically for beat synced components.
/// Going forward, it could potentially be used as the single source-of-truth for beatmap timing.
@@ -341,7 +348,7 @@ namespace osu.Game
GlobalActionContainer globalBindings;
- base.Content.Add(new SafeAreaContainer
+ base.Content.Add(SafeAreaContainer = new SafeAreaContainer
{
SafeAreaOverrideEdges = SafeAreaOverrideEdges,
RelativeSizeAxes = Axes.Both,
@@ -521,6 +528,29 @@ namespace osu.Game
/// Should be overriden per-platform to provide settings for platform-specific handlers.
public virtual SettingsSubsection CreateSettingsSubsectionFor(InputHandler handler)
{
+ // One would think that this could be moved to the `OsuGameDesktop` class, but doing so means that
+ // OsuGameTestScenes will not show any input options (as they are based on OsuGame not OsuGameDesktop).
+ //
+ // This in turn makes it hard for ruleset creators to adjust input settings while testing their ruleset
+ // within the test browser interface.
+ if (RuntimeInfo.IsDesktop)
+ {
+ switch (handler)
+ {
+ case ITabletHandler th:
+ return new TabletSettings(th);
+
+ case MouseHandler mh:
+ return new MouseSettings(mh);
+
+ case JoystickHandler jh:
+ return new JoystickSettings(jh);
+
+ case TouchHandler:
+ return new InputSection.HandlerSection(handler);
+ }
+ }
+
switch (handler)
{
case MidiHandler:
diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs
index 2be328427b..c73936da8a 100644
--- a/osu.Game/Overlays/BeatmapListingOverlay.cs
+++ b/osu.Game/Overlays/BeatmapListingOverlay.cs
@@ -115,6 +115,7 @@ namespace osu.Game.Overlays
{
filterControl.Search(query);
Show();
+ ScrollFlow.ScrollToStart();
}
protected override BeatmapListingHeader CreateHeader() => new BeatmapListingHeader();
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/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
index 59b56522a4..7f0bded806 100644
--- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs
@@ -20,6 +20,7 @@ using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
+using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Settings.Sections.Graphics
@@ -50,6 +51,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private SettingsDropdown resolutionDropdown = null!;
private SettingsDropdown displayDropdown = null!;
private SettingsDropdown windowModeDropdown = null!;
+ private SettingsCheckbox safeAreaConsiderationsCheckbox = null!;
private Bindable scalingPositionX = null!;
private Bindable scalingPositionY = null!;
@@ -101,6 +103,11 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
ItemSource = resolutions,
Current = sizeFullscreen
},
+ safeAreaConsiderationsCheckbox = new SettingsCheckbox
+ {
+ LabelText = "Shrink game to avoid cameras and notches",
+ Current = osuConfig.GetBindable(OsuSetting.SafeAreaConsiderations),
+ },
new SettingsSlider
{
LabelText = GraphicsSettingsStrings.UIScaling,
@@ -166,7 +173,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
windowModeDropdown.Current.BindValueChanged(_ =>
{
- updateDisplayModeDropdowns();
+ updateDisplaySettingsVisibility();
updateScreenModeWarning();
}, true);
@@ -191,7 +198,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
.Distinct());
}
- updateDisplayModeDropdowns();
+ updateDisplaySettingsVisibility();
}), true);
scalingMode.BindValueChanged(_ =>
@@ -221,11 +228,11 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
Scheduler.AddOnce(d =>
{
displayDropdown.Items = d;
- updateDisplayModeDropdowns();
+ updateDisplaySettingsVisibility();
}, displays);
}
- private void updateDisplayModeDropdowns()
+ private void updateDisplaySettingsVisibility()
{
if (resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen)
resolutionDropdown.Show();
@@ -236,6 +243,11 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
displayDropdown.Show();
else
displayDropdown.Hide();
+
+ if (host.Window?.SafeAreaPadding.Value.Total != Vector2.Zero)
+ safeAreaConsiderationsCheckbox.Show();
+ else
+ safeAreaConsiderationsCheckbox.Hide();
}
private void updateScreenModeWarning()
diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs
index 82fa20aa9c..9d0f43c45a 100644
--- a/osu.Game/Overlays/Toolbar/Toolbar.cs
+++ b/osu.Game/Overlays/Toolbar/Toolbar.cs
@@ -141,6 +141,8 @@ namespace osu.Game.Overlays.Toolbar
Name = "Right buttons",
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
Children = new Drawable[]
{
new Box
diff --git a/osu.Game/Overlays/Volume/VolumeControlReceptor.cs b/osu.Game/Overlays/Volume/VolumeControlReceptor.cs
index f2b637c104..624be0b25c 100644
--- a/osu.Game/Overlays/Volume/VolumeControlReceptor.cs
+++ b/osu.Game/Overlays/Volume/VolumeControlReceptor.cs
@@ -23,10 +23,14 @@ namespace osu.Game.Overlays.Volume
{
case GlobalAction.DecreaseVolume:
case GlobalAction.IncreaseVolume:
+ ActionRequested?.Invoke(e.Action);
+ return true;
+
case GlobalAction.ToggleMute:
case GlobalAction.NextVolumeMeter:
case GlobalAction.PreviousVolumeMeter:
- ActionRequested?.Invoke(e.Action);
+ if (!e.Repeat)
+ ActionRequested?.Invoke(e.Action);
return true;
}
diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs
index 46a827e03a..b0a2694a0a 100644
--- a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs
@@ -3,6 +3,9 @@
#nullable disable
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
@@ -10,9 +13,11 @@ using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
+using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
@@ -20,6 +25,7 @@ using osu.Game.Overlays;
using osu.Game.Overlays.OSD;
using osu.Game.Overlays.Settings.Sections;
using osu.Game.Rulesets.Objects;
+using osu.Game.Screens.Edit.Components.TernaryButtons;
namespace osu.Game.Rulesets.Edit
{
@@ -32,7 +38,7 @@ namespace osu.Game.Rulesets.Edit
{
private const float adjust_step = 0.1f;
- public Bindable DistanceSpacingMultiplier { get; } = new BindableDouble(1.0)
+ public BindableDouble DistanceSpacingMultiplier { get; } = new BindableDouble(1.0)
{
MinValue = 0.1,
MaxValue = 6.0,
@@ -44,10 +50,15 @@ namespace osu.Game.Rulesets.Edit
protected ExpandingToolboxContainer RightSideToolboxContainer { get; private set; }
private ExpandableSlider> distanceSpacingSlider;
+ private ExpandableButton currentDistanceSpacingButton;
[Resolved(canBeNull: true)]
private OnScreenDisplay onScreenDisplay { get; set; }
+ protected readonly Bindable DistanceSnapToggle = new Bindable();
+
+ private bool distanceSnapMomentary;
+
protected DistancedHitObjectComposer(Ruleset ruleset)
: base(ruleset)
{
@@ -74,10 +85,27 @@ namespace osu.Game.Rulesets.Edit
Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1,
Child = new EditorToolboxGroup("snapping")
{
- Child = distanceSpacingSlider = new ExpandableSlider>
+ Children = new Drawable[]
{
- Current = { BindTarget = DistanceSpacingMultiplier },
- KeyboardStep = adjust_step,
+ distanceSpacingSlider = new ExpandableSlider>
+ {
+ KeyboardStep = adjust_step,
+ // Manual binding in LoadComplete to handle one-way event flow.
+ Current = DistanceSpacingMultiplier.GetUnboundCopy(),
+ },
+ currentDistanceSpacingButton = new ExpandableButton
+ {
+ Action = () =>
+ {
+ (HitObject before, HitObject after)? objects = getObjectsOnEitherSideOfCurrentTime();
+
+ Debug.Assert(objects != null);
+
+ DistanceSpacingMultiplier.Value = ReadCurrentDistanceSnap(objects.Value.before, objects.Value.after);
+ DistanceSnapToggle.Value = TernaryState.True;
+ },
+ RelativeSizeAxes = Axes.X,
+ }
}
}
}
@@ -85,6 +113,51 @@ namespace osu.Game.Rulesets.Edit
});
}
+ private (HitObject before, HitObject after)? getObjectsOnEitherSideOfCurrentTime()
+ {
+ HitObject lastBefore = Playfield.HitObjectContainer.AliveObjects.LastOrDefault(h => h.HitObject.StartTime <= EditorClock.CurrentTime)?.HitObject;
+
+ if (lastBefore == null)
+ return null;
+
+ HitObject firstAfter = Playfield.HitObjectContainer.AliveObjects.FirstOrDefault(h => h.HitObject.StartTime >= EditorClock.CurrentTime)?.HitObject;
+
+ if (firstAfter == null)
+ return null;
+
+ if (lastBefore == firstAfter)
+ return null;
+
+ return (lastBefore, firstAfter);
+ }
+
+ protected abstract double ReadCurrentDistanceSnap(HitObject before, HitObject after);
+
+ protected override void Update()
+ {
+ base.Update();
+
+ (HitObject before, HitObject after)? objects = getObjectsOnEitherSideOfCurrentTime();
+
+ double currentSnap = objects == null
+ ? 0
+ : ReadCurrentDistanceSnap(objects.Value.before, objects.Value.after);
+
+ if (currentSnap > DistanceSpacingMultiplier.MinValue)
+ {
+ currentDistanceSpacingButton.Enabled.Value = currentDistanceSpacingButton.Expanded.Value
+ && !Precision.AlmostEquals(currentSnap, DistanceSpacingMultiplier.Value, DistanceSpacingMultiplier.Precision / 2);
+ currentDistanceSpacingButton.ContractedLabelText = $"current {currentSnap:N2}x";
+ currentDistanceSpacingButton.ExpandedLabelText = $"Use current ({currentSnap:N2}x)";
+ }
+ else
+ {
+ currentDistanceSpacingButton.Enabled.Value = false;
+ currentDistanceSpacingButton.ContractedLabelText = string.Empty;
+ currentDistanceSpacingButton.ExpandedLabelText = "Use current (unavailable)";
+ }
+ }
+
protected override void LoadComplete()
{
base.LoadComplete();
@@ -102,6 +175,45 @@ namespace osu.Game.Rulesets.Edit
EditorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue;
}, true);
+
+ // Manual binding to handle enabling distance spacing when the slider is interacted with.
+ distanceSpacingSlider.Current.BindValueChanged(spacing =>
+ {
+ DistanceSpacingMultiplier.Value = spacing.NewValue;
+ DistanceSnapToggle.Value = TernaryState.True;
+ });
+ DistanceSpacingMultiplier.BindValueChanged(spacing => distanceSpacingSlider.Current.Value = spacing.NewValue);
+ }
+ }
+
+ protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
+ {
+ new TernaryButton(DistanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
+ });
+
+ protected override bool OnKeyDown(KeyDownEvent e)
+ {
+ if (e.Repeat)
+ return false;
+
+ handleToggleViaKey(e);
+ return base.OnKeyDown(e);
+ }
+
+ protected override void OnKeyUp(KeyUpEvent e)
+ {
+ handleToggleViaKey(e);
+ base.OnKeyUp(e);
+ }
+
+ private void handleToggleViaKey(KeyboardEvent key)
+ {
+ bool altPressed = key.AltPressed;
+
+ if (altPressed != distanceSnapMomentary)
+ {
+ distanceSnapMomentary = altPressed;
+ DistanceSnapToggle.Value = DistanceSnapToggle.Value == TernaryState.False ? TernaryState.True : TernaryState.False;
}
}
@@ -111,7 +223,7 @@ namespace osu.Game.Rulesets.Edit
{
case GlobalAction.EditorIncreaseDistanceSpacing:
case GlobalAction.EditorDecreaseDistanceSpacing:
- return adjustDistanceSpacing(e.Action, adjust_step);
+ return AdjustDistanceSpacing(e.Action, adjust_step);
}
return false;
@@ -127,13 +239,13 @@ namespace osu.Game.Rulesets.Edit
{
case GlobalAction.EditorIncreaseDistanceSpacing:
case GlobalAction.EditorDecreaseDistanceSpacing:
- return adjustDistanceSpacing(e.Action, e.ScrollAmount * adjust_step);
+ return AdjustDistanceSpacing(e.Action, e.ScrollAmount * adjust_step);
}
return false;
}
- private bool adjustDistanceSpacing(GlobalAction action, float amount)
+ protected virtual bool AdjustDistanceSpacing(GlobalAction action, float amount)
{
if (DistanceSpacingMultiplier.Disabled)
return false;
@@ -143,12 +255,13 @@ namespace osu.Game.Rulesets.Edit
else if (action == GlobalAction.EditorDecreaseDistanceSpacing)
DistanceSpacingMultiplier.Value -= amount;
+ DistanceSnapToggle.Value = TernaryState.True;
return true;
}
- public virtual float GetBeatSnapDistanceAt(HitObject referenceObject)
+ public virtual float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true)
{
- return (float)(100 * EditorBeatmap.Difficulty.SliderMultiplier * referenceObject.DifficultyControlPoint.SliderVelocity / BeatSnapProvider.BeatDivisor);
+ return (float)(100 * (useReferenceSliderVelocity ? referenceObject.DifficultyControlPoint.SliderVelocity : 1) * EditorBeatmap.Difficulty.SliderMultiplier * 1 / BeatSnapProvider.BeatDivisor);
}
public virtual float DurationToDistance(HitObject referenceObject, double duration)
diff --git a/osu.Game/Rulesets/Edit/ExpandableButton.cs b/osu.Game/Rulesets/Edit/ExpandableButton.cs
new file mode 100644
index 0000000000..a66600bd58
--- /dev/null
+++ b/osu.Game/Rulesets/Edit/ExpandableButton.cs
@@ -0,0 +1,101 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Localisation;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.UserInterfaceV2;
+
+namespace osu.Game.Rulesets.Edit
+{
+ internal class ExpandableButton : RoundedButton, IExpandable
+ {
+ private float actualHeight;
+
+ public override float Height
+ {
+ get => base.Height;
+ set => base.Height = actualHeight = value;
+ }
+
+ private LocalisableString contractedLabelText;
+
+ ///
+ /// The label text to display when this button is in a contracted state.
+ ///
+ public LocalisableString ContractedLabelText
+ {
+ get => contractedLabelText;
+ set
+ {
+ if (value == contractedLabelText)
+ return;
+
+ contractedLabelText = value;
+
+ if (!Expanded.Value)
+ Text = value;
+ }
+ }
+
+ private LocalisableString expandedLabelText;
+
+ ///
+ /// The label text to display when this button is in an expanded state.
+ ///
+ public LocalisableString ExpandedLabelText
+ {
+ get => expandedLabelText;
+ set
+ {
+ if (value == expandedLabelText)
+ return;
+
+ expandedLabelText = value;
+
+ if (Expanded.Value)
+ Text = value;
+ }
+ }
+
+ public BindableBool Expanded { get; } = new BindableBool();
+
+ [Resolved(canBeNull: true)]
+ private IExpandingContainer? expandingContainer { get; set; }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ expandingContainer?.Expanded.BindValueChanged(containerExpanded =>
+ {
+ Expanded.Value = containerExpanded.NewValue;
+ }, true);
+
+ Expanded.BindValueChanged(expanded =>
+ {
+ Text = expanded.NewValue ? expandedLabelText : contractedLabelText;
+
+ if (expanded.NewValue)
+ {
+ SpriteText.Anchor = Anchor.Centre;
+ SpriteText.Origin = Anchor.Centre;
+ SpriteText.Font = OsuFont.GetFont(weight: FontWeight.Bold);
+ base.Height = actualHeight;
+ Background.Show();
+ }
+ else
+ {
+ SpriteText.Anchor = Anchor.CentreLeft;
+ SpriteText.Origin = Anchor.CentreLeft;
+ SpriteText.Font = OsuFont.GetFont(weight: FontWeight.Regular);
+ base.Height = actualHeight / 2;
+ Background.Hide();
+ }
+ }, true);
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
index 3bed835854..520fcb0290 100644
--- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
@@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Edit
protected override bool OnKeyDown(KeyDownEvent e)
{
- if (e.ControlPressed || e.AltPressed || e.SuperPressed)
+ if (e.ControlPressed || e.AltPressed || e.SuperPressed || e.ShiftPressed)
return false;
if (checkLeftToggleFromKey(e.Key, out int leftIndex))
diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs
index 5ad1cc78ff..6fbd994e23 100644
--- a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs
+++ b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs
@@ -27,8 +27,9 @@ namespace osu.Game.Rulesets.Edit
/// Retrieves the distance between two points within a timing point that are one beat length apart.
///
/// An object to be used as a reference point for this operation.
+ /// Whether the 's slider velocity should be factored into the returned distance.
/// The distance between two points residing in the timing point that are one beat length apart.
- float GetBeatSnapDistanceAt(HitObject referenceObject);
+ float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true);
///
/// Converts a duration to a distance without applying any snapping.
diff --git a/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObjects.cs b/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObjects.cs
deleted file mode 100644
index 7f926dd8b8..0000000000
--- a/osu.Game/Rulesets/Mods/IApplicableToDrawableHitObjects.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.Collections.Generic;
-using osu.Framework.Extensions.IEnumerableExtensions;
-using osu.Game.Rulesets.Objects.Drawables;
-
-namespace osu.Game.Rulesets.Mods
-{
- [Obsolete(@"Use the singular version IApplicableToDrawableHitObject instead.")] // Can be removed 20211216
- public interface IApplicableToDrawableHitObjects : IApplicableToDrawableHitObject
- {
- void ApplyToDrawableHitObjects(IEnumerable drawables);
-
- void IApplicableToDrawableHitObject.ApplyToDrawableHitObject(DrawableHitObject drawable) => ApplyToDrawableHitObjects(drawable.Yield());
- }
-}
diff --git a/osu.Game/Rulesets/Mods/ICreateReplay.cs b/osu.Game/Rulesets/Mods/ICreateReplay.cs
deleted file mode 100644
index 1e5eeca92c..0000000000
--- a/osu.Game/Rulesets/Mods/ICreateReplay.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.Collections.Generic;
-using osu.Game.Beatmaps;
-using osu.Game.Scoring;
-
-namespace osu.Game.Rulesets.Mods
-{
- [Obsolete("Use ICreateReplayData instead")] // Can be removed 20220929
- public interface ICreateReplay : ICreateReplayData
- {
- public Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods);
-
- ModReplayData ICreateReplayData.CreateReplayData(IBeatmap beatmap, IReadOnlyList mods)
- {
- var replayScore = CreateReplayScore(beatmap, mods);
- return new ModReplayData(replayScore.Replay, new ModCreatedUser { Username = replayScore.ScoreInfo.User.Username });
- }
- }
-}
diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs
index e4c91d3037..98df540de4 100644
--- a/osu.Game/Rulesets/Mods/Mod.cs
+++ b/osu.Game/Rulesets/Mods/Mod.cs
@@ -101,9 +101,6 @@ namespace osu.Game.Rulesets.Mods
[JsonIgnore]
public virtual bool ValidForMultiplayerAsFreeMod => true;
- [Obsolete("Going forward, the concept of \"ranked\" doesn't exist. The only exceptions are automation mods, which should now override and set UserPlayable to false.")] // Can be removed 20211009
- public virtual bool Ranked => false;
-
///
/// Whether this mod requires configuration to apply changes to the game.
///
diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs
index 6cafe0716d..83afda3a28 100644
--- a/osu.Game/Rulesets/Mods/ModAutoplay.cs
+++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs
@@ -8,7 +8,6 @@ using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Replays;
-using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mods
{
@@ -33,16 +32,6 @@ namespace osu.Game.Rulesets.Mods
public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0;
- [Obsolete("Override CreateReplayData(IBeatmap, IReadOnlyList) instead")] // Can be removed 20220929
- public virtual Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { Replay = new Replay() };
-
- public virtual ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods)
- {
-#pragma warning disable CS0618
- var replayScore = CreateReplayScore(beatmap, mods);
-#pragma warning restore CS0618
-
- return new ModReplayData(replayScore.Replay, new ModCreatedUser { Username = replayScore.ScoreInfo.User.Username });
- }
+ public virtual ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) => new ModReplayData(new Replay(), new ModCreatedUser { Username = @"autoplay" });
}
}
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index dec68a6c22..e5150576f2 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -196,18 +196,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
updateState(State.Value, true);
}
- ///
- /// Applies a hit object to be represented by this .
- ///
- [Obsolete("Use either overload of Apply that takes a single argument of type HitObject or HitObjectLifetimeEntry")] // Can be removed 20211021.
- public void Apply([NotNull] HitObject hitObject, [CanBeNull] HitObjectLifetimeEntry lifetimeEntry)
- {
- if (lifetimeEntry != null)
- Apply(lifetimeEntry);
- else
- Apply(hitObject);
- }
-
///
/// Applies a new to be represented by this .
/// A new is automatically created and applied to this .
@@ -278,6 +266,10 @@ namespace osu.Game.Rulesets.Objects.Drawables
updateState(ArmedState.Miss, true);
else
updateState(ArmedState.Idle, true);
+
+ // Combo colour may have been applied via a bindable flow while no object entry was attached.
+ // Update here to ensure we're in a good state.
+ UpdateComboColour();
}
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
index b289299a63..930ee0448f 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
@@ -199,8 +199,8 @@ namespace osu.Game.Rulesets.Objects.Legacy
if (stringAddBank == @"none")
stringAddBank = null;
- bankInfo.Normal = stringBank;
- bankInfo.Add = string.IsNullOrEmpty(stringAddBank) ? stringBank : stringAddBank;
+ bankInfo.BankForNormal = stringBank;
+ bankInfo.BankForAdditions = string.IsNullOrEmpty(stringAddBank) ? stringBank : stringAddBank;
if (split.Length > 2)
bankInfo.CustomSampleBank = Parsing.ParseInt(split[2]);
@@ -447,32 +447,54 @@ namespace osu.Game.Rulesets.Objects.Legacy
var soundTypes = new List
{
- new LegacyHitSampleInfo(HitSampleInfo.HIT_NORMAL, bankInfo.Normal, bankInfo.Volume, bankInfo.CustomSampleBank,
+ new LegacyHitSampleInfo(HitSampleInfo.HIT_NORMAL, bankInfo.BankForNormal, bankInfo.Volume, bankInfo.CustomSampleBank,
// if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample.
// None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds
type != LegacyHitSoundType.None && !type.HasFlagFast(LegacyHitSoundType.Normal))
};
if (type.HasFlagFast(LegacyHitSoundType.Finish))
- soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_FINISH, bankInfo.Add, bankInfo.Volume, bankInfo.CustomSampleBank));
+ soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_FINISH, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank));
if (type.HasFlagFast(LegacyHitSoundType.Whistle))
- soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_WHISTLE, bankInfo.Add, bankInfo.Volume, bankInfo.CustomSampleBank));
+ soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_WHISTLE, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank));
if (type.HasFlagFast(LegacyHitSoundType.Clap))
- soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_CLAP, bankInfo.Add, bankInfo.Volume, bankInfo.CustomSampleBank));
+ soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_CLAP, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank));
return soundTypes;
}
private class SampleBankInfo
{
+ ///
+ /// An optional overriding filename which causes all bank/sample specifications to be ignored.
+ ///
public string Filename;
- public string Normal;
- public string Add;
+ ///
+ /// The bank identifier to use for the base ("hitnormal") sample.
+ /// Transferred to when appropriate.
+ ///
+ public string BankForNormal;
+
+ ///
+ /// The bank identifier to use for additions ("hitwhistle", "hitfinish", "hitclap").
+ /// Transferred to when appropriate.
+ ///
+ public string BankForAdditions;
+
+ ///
+ /// Hit sample volume (0-100).
+ /// See .
+ ///
public int Volume;
+ ///
+ /// The index of the custom sample bank. Is only used if 2 or above for "reasons".
+ /// This will add a suffix to lookups, allowing extended bank lookups (ie. "normal-hitnormal-2").
+ /// See .
+ ///
public int CustomSampleBank;
public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone();
@@ -503,7 +525,8 @@ namespace osu.Game.Rulesets.Objects.Legacy
public sealed override HitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newSuffix = default, Optional newVolume = default)
=> With(newName, newBank, newVolume);
- public virtual LegacyHitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newVolume = default, Optional newCustomSampleBank = default,
+ public virtual LegacyHitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newVolume = default,
+ Optional newCustomSampleBank = default,
Optional newIsLayered = default)
=> new LegacyHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newVolume.GetOr(Volume), newCustomSampleBank.GetOr(CustomSampleBank), newIsLayered.GetOr(IsLayered));
@@ -537,7 +560,8 @@ namespace osu.Game.Rulesets.Objects.Legacy
Path.ChangeExtension(Filename, null)
};
- public sealed override LegacyHitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newVolume = default, Optional newCustomSampleBank = default,
+ public sealed override LegacyHitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newVolume = default,
+ Optional newCustomSampleBank = default,
Optional newIsLayered = default)
=> new FileHitSampleInfo(Filename, newVolume.GetOr(Volume));
diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs
index fdbcd0ed1e..e2b8cd2c4e 100644
--- a/osu.Game/Rulesets/RulesetStore.cs
+++ b/osu.Game/Rulesets/RulesetStore.cs
@@ -158,7 +158,7 @@ namespace osu.Game.Rulesets
}
catch (Exception e)
{
- LogFailedLoad(assembly.FullName, e);
+ LogFailedLoad(assembly.GetName().Name.Split('.').Last(), e);
}
}
@@ -168,14 +168,14 @@ namespace osu.Game.Rulesets
GC.SuppressFinalize(this);
}
- protected virtual void Dispose(bool disposing)
+ protected void Dispose(bool disposing)
{
AppDomain.CurrentDomain.AssemblyResolve -= resolveRulesetDependencyAssembly;
}
protected void LogFailedLoad(string name, Exception exception)
{
- Logger.Log($"Could not load ruleset {name}. Please check for an update from the developer.", level: LogLevel.Error);
+ Logger.Log($"Could not load ruleset \"{name}\". Please check for an update from the developer.", level: LogLevel.Error);
Logger.Log($"Ruleset load failed: {exception}");
}
diff --git a/osu.Game/Screens/Edit/BindableBeatDivisor.cs b/osu.Game/Screens/Edit/BindableBeatDivisor.cs
index a7faf961cf..aa8e202e22 100644
--- a/osu.Game/Screens/Edit/BindableBeatDivisor.cs
+++ b/osu.Game/Screens/Edit/BindableBeatDivisor.cs
@@ -25,6 +25,26 @@ namespace osu.Game.Screens.Edit
BindValueChanged(_ => ensureValidDivisor());
}
+ ///
+ /// Set a divisor, updating the valid divisor range appropriately.
+ ///
+ /// The intended divisor.
+ public void SetArbitraryDivisor(int divisor)
+ {
+ // If the current valid divisor range doesn't contain the proposed value, attempt to find one which does.
+ if (!ValidDivisors.Value.Presets.Contains(divisor))
+ {
+ if (BeatDivisorPresetCollection.COMMON.Presets.Contains(divisor))
+ ValidDivisors.Value = BeatDivisorPresetCollection.COMMON;
+ else if (BeatDivisorPresetCollection.TRIPLETS.Presets.Contains(divisor))
+ ValidDivisors.Value = BeatDivisorPresetCollection.TRIPLETS;
+ else
+ ValidDivisors.Value = BeatDivisorPresetCollection.Custom(divisor);
+ }
+
+ Value = divisor;
+ }
+
private void updateBindableProperties()
{
ensureValidDivisor();
diff --git a/osu.Game/Screens/Edit/Components/CircularButton.cs b/osu.Game/Screens/Edit/Components/CircularButton.cs
deleted file mode 100644
index 74e4162102..0000000000
--- a/osu.Game/Screens/Edit/Components/CircularButton.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-// 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.Graphics.UserInterface;
-using osuTK;
-
-namespace osu.Game.Screens.Edit.Components
-{
- public class CircularButton : OsuButton
- {
- private const float width = 125;
- private const float height = 30;
-
- public CircularButton()
- {
- Size = new Vector2(width, height);
- }
-
- protected override void Update()
- {
- base.Update();
- Content.CornerRadius = DrawHeight / 2f;
- Content.CornerExponent = 2;
- }
- }
-}
diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs
index 19ea2162a3..6dca799549 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs
@@ -209,6 +209,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
}
+ protected override bool OnKeyDown(KeyDownEvent e)
+ {
+ if (e.ShiftPressed && e.Key >= Key.Number1 && e.Key <= Key.Number9)
+ {
+ beatDivisor.SetArbitraryDivisor(e.Key - Key.Number0);
+ return true;
+ }
+
+ return base.OnKeyDown(e);
+ }
+
internal class DivisorDisplay : OsuAnimatedButton, IHasPopover
{
public BindableBeatDivisor BeatDivisor { get; } = new BindableBeatDivisor();
@@ -306,17 +317,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
return;
}
- if (!BeatDivisor.ValidDivisors.Value.Presets.Contains(divisor))
- {
- if (BeatDivisorPresetCollection.COMMON.Presets.Contains(divisor))
- BeatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.COMMON;
- else if (BeatDivisorPresetCollection.TRIPLETS.Presets.Contains(divisor))
- BeatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.TRIPLETS;
- else
- BeatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.Custom(divisor);
- }
-
- BeatDivisor.Value = divisor;
+ BeatDivisor.SetArbitraryDivisor(divisor);
this.HidePopover();
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs
index 98079116cd..6e54e98740 100644
--- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs
@@ -53,9 +53,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
float maxDistance = new Vector2(dx, dy).Length;
int requiredCircles = Math.Min(MaxIntervals, (int)(maxDistance / DistanceBetweenTicks));
+ // We need to offset the drawn lines to the next valid snap for the currently selected divisor.
+ //
+ // Picture the scenario where the user has just placed an object on a 1/2 snap, then changes to
+ // 1/3 snap and expects to be able to place the next object on a valid 1/3 snap, regardless of the
+ // fact that the 1/2 snap reference object is not valid for 1/3 snapping.
+ float offset = SnapProvider.FindSnappedDistance(ReferenceObject, 0);
+
for (int i = 0; i < requiredCircles; i++)
{
- float diameter = (i + 1) * DistanceBetweenTicks * 2;
+ float diameter = (offset + (i + 1) * DistanceBetweenTicks) * 2;
AddInternal(new Ring(ReferenceObject, GetColourForIndexFromPlacement(i))
{
diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
index 69c7fc2775..c179e7f0c2 100644
--- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
@@ -97,7 +97,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void updateSpacing()
{
float distanceSpacingMultiplier = (float)DistanceSpacingMultiplier.Value;
- float beatSnapDistance = SnapProvider.GetBeatSnapDistanceAt(ReferenceObject);
+ float beatSnapDistance = SnapProvider.GetBeatSnapDistanceAt(ReferenceObject, false);
DistanceBetweenTicks = beatSnapDistance * distanceSpacingMultiplier;
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
index a73ada76f5..3a93499527 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
@@ -250,7 +250,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private void seekTrackToCurrent()
{
- double target = Current / Content.DrawWidth * editorClock.TrackLength;
+ double target = TimeAtPosition(Current);
editorClock.Seek(Math.Min(editorClock.TrackLength, target));
}
@@ -264,7 +264,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (handlingDragInput)
editorClock.Stop();
- ScrollTo((float)(editorClock.CurrentTime / editorClock.TrackLength) * Content.DrawWidth, false);
+ float position = PositionAtTime(editorClock.CurrentTime);
+ ScrollTo(position, false);
}
protected override bool OnMouseDown(MouseDownEvent e)
diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs
index 3dfc7010f3..912681e114 100644
--- a/osu.Game/Screens/Edit/Editor.cs
+++ b/osu.Game/Screens/Edit/Editor.cs
@@ -304,6 +304,7 @@ namespace osu.Game.Screens.Edit
cutMenuItem = new EditorMenuItem("Cut", MenuItemType.Standard, Cut),
copyMenuItem = new EditorMenuItem("Copy", MenuItemType.Standard, Copy),
pasteMenuItem = new EditorMenuItem("Paste", MenuItemType.Standard, Paste),
+ cloneMenuItem = new EditorMenuItem("Clone", MenuItemType.Standard, Clone),
}
},
new MenuItem("View")
@@ -575,6 +576,10 @@ namespace osu.Game.Screens.Edit
this.Exit();
return true;
+ case GlobalAction.EditorCloneSelection:
+ Clone();
+ return true;
+
case GlobalAction.EditorComposeMode:
Mode.Value = EditorScreenMode.Compose;
return true;
@@ -741,6 +746,7 @@ namespace osu.Game.Screens.Edit
private EditorMenuItem cutMenuItem;
private EditorMenuItem copyMenuItem;
+ private EditorMenuItem cloneMenuItem;
private EditorMenuItem pasteMenuItem;
private readonly BindableWithCurrent canCut = new BindableWithCurrent();
@@ -750,7 +756,11 @@ namespace osu.Game.Screens.Edit
private void setUpClipboardActionAvailability()
{
canCut.Current.BindValueChanged(cut => cutMenuItem.Action.Disabled = !cut.NewValue, true);
- canCopy.Current.BindValueChanged(copy => copyMenuItem.Action.Disabled = !copy.NewValue, true);
+ canCopy.Current.BindValueChanged(copy =>
+ {
+ copyMenuItem.Action.Disabled = !copy.NewValue;
+ cloneMenuItem.Action.Disabled = !copy.NewValue;
+ }, true);
canPaste.Current.BindValueChanged(paste => pasteMenuItem.Action.Disabled = !paste.NewValue, true);
}
@@ -765,6 +775,21 @@ namespace osu.Game.Screens.Edit
protected void Copy() => currentScreen?.Copy();
+ protected void Clone()
+ {
+ // Avoid attempting to clone if copying is not available (as it may result in pasting something unexpected).
+ if (!canCopy.Value)
+ return;
+
+ // This is an initial implementation just to get an idea of how people used this function.
+ // There are a couple of differences from osu!stable's implementation which will require more work to match:
+ // - The "clipboard" is not populated during the duplication process.
+ // - The duplicated hitobjects are inserted after the original pattern (add one beat_length and then quantize using beat snap).
+ // - The duplicated hitobjects are selected (but this is also applied for all paste operations so should be changed there).
+ Copy();
+ Paste();
+ }
+
protected void Paste() => currentScreen?.Paste();
#endregion
diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs
index e9b50f94f7..3efd74d2c8 100644
--- a/osu.Game/Screens/Menu/OsuLogo.cs
+++ b/osu.Game/Screens/Menu/OsuLogo.cs
@@ -113,7 +113,7 @@ namespace osu.Game.Screens.Menu
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
- logoBounceContainer = new DragContainer
+ logoBounceContainer = new Container
{
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
@@ -407,27 +407,24 @@ namespace osu.Game.Screens.Menu
impactContainer.ScaleTo(1.12f, 250);
}
- private class DragContainer : Container
+ public override bool DragBlocksClick => false;
+
+ protected override bool OnDragStart(DragStartEvent e) => true;
+
+ protected override void OnDrag(DragEvent e)
{
- public override bool DragBlocksClick => false;
+ Vector2 change = e.MousePosition - e.MouseDownPosition;
- protected override bool OnDragStart(DragStartEvent e) => true;
+ // Diminish the drag distance as we go further to simulate "rubber band" feeling.
+ change *= change.Length <= 0 ? 0 : MathF.Pow(change.Length, 0.6f) / change.Length;
- protected override void OnDrag(DragEvent e)
- {
- Vector2 change = e.MousePosition - e.MouseDownPosition;
+ logoBounceContainer.MoveTo(change);
+ }
- // Diminish the drag distance as we go further to simulate "rubber band" feeling.
- change *= change.Length <= 0 ? 0 : MathF.Pow(change.Length, 0.6f) / change.Length;
-
- this.MoveTo(change);
- }
-
- protected override void OnDragEnd(DragEndEvent e)
- {
- this.MoveTo(Vector2.Zero, 800, Easing.OutElastic);
- base.OnDragEnd(e);
- }
+ protected override void OnDragEnd(DragEndEvent e)
+ {
+ logoBounceContainer.MoveTo(Vector2.Zero, 800, Easing.OutElastic);
+ base.OnDragEnd(e);
}
}
}
diff --git a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs
index 30b420441c..45d0cf8462 100644
--- a/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs
+++ b/osu.Game/Screens/Play/HUD/DefaultSongProgress.cs
@@ -9,7 +9,6 @@ using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
-using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Screens.Play.HUD
@@ -45,12 +44,6 @@ namespace osu.Game.Screens.Play.HUD
[Resolved]
private DrawableRuleset? drawableRuleset { get; set; }
- [Resolved]
- private OsuConfigManager config { get; set; } = null!;
-
- [Resolved]
- private SkinManager skinManager { get; set; } = null!;
-
public DefaultSongProgress()
{
RelativeSizeAxes = Axes.X;
@@ -100,47 +93,6 @@ namespace osu.Game.Screens.Play.HUD
{
AllowSeeking.BindValueChanged(_ => updateBarVisibility(), true);
ShowGraph.BindValueChanged(_ => updateGraphVisibility(), true);
-
- migrateSettingFromConfig();
- }
-
- ///
- /// This setting has been migrated to a per-component level.
- /// Only take the value from the config if it is in a non-default state (then reset it to default so it only applies once).
- ///
- /// Can be removed 20221027.
- ///
- private void migrateSettingFromConfig()
- {
- Bindable configShowGraph = config.GetBindable(OsuSetting.ShowProgressGraph);
-
- if (!configShowGraph.IsDefault)
- {
- ShowGraph.Value = configShowGraph.Value;
-
- // This is pretty ugly, but the only way to make this stick...
- var skinnableTarget = this.FindClosestParent();
-
- if (skinnableTarget != null)
- {
- // If the skin is not mutable, a mutable instance will be created, causing this migration logic to run again on the correct skin.
- // Therefore we want to avoid resetting the config value on this invocation.
- if (skinManager.EnsureMutableSkin())
- return;
-
- // If `EnsureMutableSkin` actually changed the skin, default layout may take a frame to apply.
- // See `SkinnableTargetComponentsContainer`'s use of ScheduleAfterChildren.
- ScheduleAfterChildren(() =>
- {
- var skin = skinManager.CurrentSkin.Value;
- skin.UpdateDrawableTarget(skinnableTarget);
-
- skinManager.Save(skin);
- });
-
- configShowGraph.SetDefault();
- }
- }
}
protected override void PopIn()
diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs
index d6b9c62369..e7b2ce1672 100644
--- a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs
+++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs
@@ -279,6 +279,8 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
switch (style)
{
case LabelStyles.None:
+ labelEarly.Clear();
+ labelLate.Clear();
break;
case LabelStyles.Icons:
diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs
index 7833c2d7fa..2791f5ff8f 100644
--- a/osu.Game/Screens/Play/HUDOverlay.cs
+++ b/osu.Game/Screens/Play/HUDOverlay.cs
@@ -39,9 +39,16 @@ namespace osu.Game.Screens.Play
///
public float BottomScoringElementsHeight { get; private set; }
- // HUD uses AlwaysVisible on child components so they can be in an updated state for next display.
- // Without blocking input, this would also allow them to be interacted with in such a state.
- public override bool PropagatePositionalInputSubTree => ShowHud.Value;
+ protected override bool ShouldBeConsideredForInput(Drawable child)
+ {
+ // HUD uses AlwaysVisible on child components so they can be in an updated state for next display.
+ // Without blocking input, this would also allow them to be interacted with in such a state.
+ if (ShowHud.Value)
+ return base.ShouldBeConsideredForInput(child);
+
+ // hold to quit button should always be interactive.
+ return child == bottomRightElements;
+ }
public readonly KeyCounterDisplay KeyCounter;
public readonly ModDisplay ModDisplay;
diff --git a/osu.Game/Screens/Play/PlayerSettings/PlayerCheckbox.cs b/osu.Game/Screens/Play/PlayerSettings/PlayerCheckbox.cs
index dc09676254..cea03d2155 100644
--- a/osu.Game/Screens/Play/PlayerSettings/PlayerCheckbox.cs
+++ b/osu.Game/Screens/Play/PlayerSettings/PlayerCheckbox.cs
@@ -1,22 +1,27 @@
// 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;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays.Settings;
namespace osu.Game.Screens.Play.PlayerSettings
{
- public class PlayerCheckbox : OsuCheckbox
+ public class PlayerCheckbox : SettingsCheckbox
{
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ protected override Drawable CreateControl() => new PlayerCheckboxControl();
+
+ public class PlayerCheckboxControl : OsuCheckbox
{
- Nub.AccentColour = colours.Yellow;
- Nub.GlowingAccentColour = colours.YellowLighter;
- Nub.GlowColour = colours.YellowDark;
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ Nub.AccentColour = colours.Yellow;
+ Nub.GlowingAccentColour = colours.YellowLighter;
+ Nub.GlowColour = colours.YellowDark;
+ }
}
}
}
diff --git a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs
index e55af0bba7..bb3360acec 100644
--- a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs
+++ b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs
@@ -1,12 +1,9 @@
// 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.Configuration;
-using osu.Game.Graphics.Sprites;
using osu.Game.Localisation;
namespace osu.Game.Screens.Play.PlayerSettings
@@ -24,26 +21,16 @@ namespace osu.Game.Screens.Play.PlayerSettings
{
Children = new Drawable[]
{
- new OsuSpriteText
- {
- Text = GameplaySettingsStrings.BackgroundDim
- },
dimSliderBar = new PlayerSliderBar
{
+ LabelText = GameplaySettingsStrings.BackgroundDim,
DisplayAsPercentage = true
},
- new OsuSpriteText
- {
- Text = GameplaySettingsStrings.BackgroundBlur
- },
blurSliderBar = new PlayerSliderBar
{
+ LabelText = GameplaySettingsStrings.BackgroundBlur,
DisplayAsPercentage = true
},
- new OsuSpriteText
- {
- Text = "Toggles:"
- },
showStoryboardToggle = new PlayerCheckbox { LabelText = GraphicsSettingsStrings.StoryboardVideo },
beatmapSkinsToggle = new PlayerCheckbox { LabelText = SkinSettingsStrings.BeatmapSkins },
beatmapColorsToggle = new PlayerCheckbox { LabelText = SkinSettingsStrings.BeatmapColours },
diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs
index e3ac054d1b..5bbd260d3f 100644
--- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs
+++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs
@@ -36,12 +36,6 @@ namespace osu.Game.Screens.Ranking.Statistics
///
public readonly bool RequiresHitEvents;
- [Obsolete("Use constructor which takes creation function instead.")] // Can be removed 20220803.
- public StatisticItem([NotNull] string name, [NotNull] Drawable content, [CanBeNull] Dimension dimension = null)
- : this(name, () => content, true, dimension)
- {
- }
-
///
/// Creates a new , to be displayed inside a in the results screen.
///
diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs
index a8cb06b888..3b694dbf43 100644
--- a/osu.Game/Screens/Select/BeatmapCarousel.cs
+++ b/osu.Game/Screens/Select/BeatmapCarousel.cs
@@ -1048,7 +1048,7 @@ namespace osu.Game.Screens.Select
protected override void PerformSelection()
{
- if (LastSelected == null || LastSelected.Filtered.Value)
+ if (LastSelected == null)
carousel?.SelectNextRandom();
else
base.PerformSelection();
diff --git a/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs b/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs
index 61109829f3..6366fc8050 100644
--- a/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs
+++ b/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs
@@ -108,10 +108,35 @@ namespace osu.Game.Screens.Select.Carousel
PerformSelection();
}
+ ///
+ /// Finds the item this group would select next if it attempted selection
+ ///
+ /// An unfiltered item nearest to the last selected one or null if all items are filtered
protected virtual CarouselItem GetNextToSelect()
{
- return Items.Skip(lastSelectedIndex).FirstOrDefault(i => !i.Filtered.Value) ??
- Items.Reverse().Skip(Items.Count - lastSelectedIndex).FirstOrDefault(i => !i.Filtered.Value);
+ if (Items.Count == 0)
+ return null;
+
+ int forwardsIndex = lastSelectedIndex;
+ int backwardsIndex = Math.Min(lastSelectedIndex, Items.Count - 1);
+
+ while (true)
+ {
+ bool hasBackwards = backwardsIndex >= 0 && backwardsIndex < Items.Count;
+ bool hasForwards = forwardsIndex < Items.Count;
+
+ if (!hasBackwards && !hasForwards)
+ return null;
+
+ if (hasForwards && !Items[forwardsIndex].Filtered.Value)
+ return Items[forwardsIndex];
+
+ if (hasBackwards && !Items[backwardsIndex].Filtered.Value)
+ return Items[backwardsIndex];
+
+ forwardsIndex++;
+ backwardsIndex--;
+ }
}
protected virtual void PerformSelection()
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/KiaiFlashingDrawable.cs b/osu.Game/Skinning/LegacyKiaiFlashingDrawable.cs
similarity index 81%
rename from osu.Game.Rulesets.Osu/Skinning/Legacy/KiaiFlashingDrawable.cs
rename to osu.Game/Skinning/LegacyKiaiFlashingDrawable.cs
index 152ed5c3d9..2bcdd5b5a1 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/KiaiFlashingDrawable.cs
+++ b/osu.Game/Skinning/LegacyKiaiFlashingDrawable.cs
@@ -7,15 +7,15 @@ using osu.Framework.Graphics;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
-namespace osu.Game.Rulesets.Osu.Skinning.Legacy
+namespace osu.Game.Skinning
{
- internal class KiaiFlashingDrawable : BeatSyncedContainer
+ public class LegacyKiaiFlashingDrawable : BeatSyncedContainer
{
private readonly Drawable flashingDrawable;
- private const float flash_opacity = 0.3f;
+ private const float flash_opacity = 0.55f;
- public KiaiFlashingDrawable(Func creationFunc)
+ public LegacyKiaiFlashingDrawable(Func creationFunc)
{
AutoSizeAxes = Axes.Both;
@@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
flashingDrawable
.FadeTo(flash_opacity)
.Then()
- .FadeOut(timingPoint.BeatLength * 0.75f);
+ .FadeOut(Math.Max(80, timingPoint.BeatLength - 80), Easing.OutSine);
}
}
}
diff --git a/osu.Game/Skinning/SkinConfiguration.cs b/osu.Game/Skinning/SkinConfiguration.cs
index 4e5d96ccb8..a9f660312e 100644
--- a/osu.Game/Skinning/SkinConfiguration.cs
+++ b/osu.Game/Skinning/SkinConfiguration.cs
@@ -66,8 +66,6 @@ namespace osu.Game.Skinning
}
}
- void IHasComboColours.AddComboColours(params Color4[] colours) => CustomComboColours.AddRange(colours);
-
public Dictionary CustomColours { get; } = new Dictionary();
public readonly Dictionary ConfigDictionary = new Dictionary();
diff --git a/osu.Game/Skinning/SkinImporter.cs b/osu.Game/Skinning/SkinImporter.cs
index 701dcdfc2d..f594a7b2d2 100644
--- a/osu.Game/Skinning/SkinImporter.cs
+++ b/osu.Game/Skinning/SkinImporter.cs
@@ -4,11 +4,9 @@
using System;
using System.Collections.Generic;
using System.IO;
-using System.Linq;
using System.Text;
using System.Threading;
using Newtonsoft.Json;
-using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Database;
@@ -33,9 +31,6 @@ namespace osu.Game.Skinning
this.skinResources = skinResources;
modelManager = new ModelManager(storage, realm);
-
- // can be removed 20220420.
- populateMissingHashes();
}
public override IEnumerable HandledExtensions => new[] { ".osk" };
@@ -158,18 +153,6 @@ namespace osu.Game.Skinning
}
modelManager.ReplaceFile(existingFile, stream, realm);
-
- // can be removed 20220502.
- if (!ensureIniWasUpdated(item))
- {
- Logger.Log($"Skin {item}'s skin.ini had issues and has been removed. Please report this and provide the problematic skin.", LoggingTarget.Database, LogLevel.Important);
-
- var existingIni = item.GetFile(@"skin.ini");
- if (existingIni != null)
- item.Files.Remove(existingIni);
-
- writeNewSkinIni();
- }
}
}
@@ -194,38 +177,6 @@ namespace osu.Game.Skinning
}
}
- private bool ensureIniWasUpdated(SkinInfo item)
- {
- // This is a final consistency check to ensure that hash computation doesn't enter an infinite loop.
- // With other changes to the surrounding code this should never be hit, but until we are 101% sure that there
- // are no other cases let's avoid a hard startup crash by bailing and alerting.
-
- var instance = createInstance(item);
-
- return instance.Configuration.SkinInfo.Name == item.Name;
- }
-
- private void populateMissingHashes()
- {
- Realm.Run(realm =>
- {
- var skinsWithoutHashes = realm.All().Where(i => !i.Protected && string.IsNullOrEmpty(i.Hash)).ToArray();
-
- foreach (SkinInfo skin in skinsWithoutHashes)
- {
- try
- {
- realm.Write(_ => skin.Hash = ComputeHash(skin));
- }
- catch (Exception e)
- {
- modelManager.Delete(skin);
- Logger.Error(e, $"Existing skin {skin} has been deleted during hash recomputation due to being invalid");
- }
- }
- });
- }
-
private Skin createInstance(SkinInfo item) => item.CreateInstance(skinResources);
public void Save(Skin skin)
diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs
index ced72aa593..0e7bb72162 100644
--- a/osu.Game/Tests/Visual/EditorTestScene.cs
+++ b/osu.Game/Tests/Visual/EditorTestScene.cs
@@ -110,6 +110,8 @@ namespace osu.Game.Tests.Visual
public new void Paste() => base.Paste();
+ public new void Clone() => base.Clone();
+
public new void SwitchToDifficulty(BeatmapInfo beatmapInfo) => base.SwitchToDifficulty(beatmapInfo);
public new void CreateNewDifficulty(RulesetInfo rulesetInfo) => base.CreateNewDifficulty(rulesetInfo);
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 22474c0592..8d45ebec57 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -35,7 +35,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index cf70b65578..76d2e727c8 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -62,7 +62,7 @@
-
+
@@ -82,7 +82,7 @@
-
+