diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt
index 022da0a2ea..03fd21829d 100644
--- a/CodeAnalysis/BannedSymbols.txt
+++ b/CodeAnalysis/BannedSymbols.txt
@@ -15,6 +15,8 @@ M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Gen
M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks.
P:System.Threading.Tasks.Task`1.Result;Don't use Task.Result. Use Task.GetResultSafely() to ensure we avoid deadlocks.
M:System.Threading.ManualResetEventSlim.Wait();Specify a timeout to avoid waiting forever.
+M:System.Char.ToLower(System.Char);char.ToLower() changes behaviour depending on CultureInfo.CurrentCulture. Use char.ToLowerInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture.
+M:System.Char.ToUpper(System.Char);char.ToUpper() changes behaviour depending on CultureInfo.CurrentCulture. Use char.ToUpperInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture.
M:System.String.ToLower();string.ToLower() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToLowerInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString.
M:System.String.ToUpper();string.ToUpper() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToUpperInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString.
M:Humanizer.InflectorExtensions.Pascalize(System.String);Humanizer's .Pascalize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToPascalCase() instead.
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 3f4c8e2d24..8711ceec64 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,8 +51,8 @@
-
-
+
+
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.Tests/Mods/TestSceneCatchModFlashlight.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModFlashlight.cs
new file mode 100644
index 0000000000..538fc7fac6
--- /dev/null
+++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModFlashlight.cs
@@ -0,0 +1,23 @@
+// 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.Catch.Mods;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Catch.Tests.Mods
+{
+ public class TestSceneCatchModFlashlight : ModTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
+
+ [TestCase(1f)]
+ [TestCase(0.5f)]
+ [TestCase(1.25f)]
+ [TestCase(1.5f)]
+ public void TestSizeMultiplier(float sizeMultiplier) => CreateModTest(new ModTestData { Mod = new CatchModFlashlight { SizeMultiplier = { Value = sizeMultiplier } }, PassCondition = () => true });
+
+ [Test]
+ public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new CatchModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
+ }
+}
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/CatchEditorPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfieldAdjustmentContainer.cs
new file mode 100644
index 0000000000..0a0f91c781
--- /dev/null
+++ b/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfieldAdjustmentContainer.cs
@@ -0,0 +1,49 @@
+// 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.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Catch.UI;
+using osu.Game.Rulesets.UI;
+using osuTK;
+
+namespace osu.Game.Rulesets.Catch.Edit
+{
+ public class CatchEditorPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
+ {
+ protected override Container Content => content;
+ private readonly Container content;
+
+ public CatchEditorPlayfieldAdjustmentContainer()
+ {
+ Anchor = Anchor.TopCentre;
+ Origin = Anchor.TopCentre;
+ Size = new Vector2(0.8f, 0.9f);
+
+ InternalChild = new ScalingContainer
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Child = content = new Container { RelativeSizeAxes = Axes.Both },
+ };
+ }
+
+ private class ScalingContainer : Container
+ {
+ public ScalingContainer()
+ {
+ RelativeSizeAxes = Axes.Y;
+ Width = CatchPlayfield.WIDTH;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ Scale = new Vector2(Math.Min(Parent.ChildSize.X / CatchPlayfield.WIDTH, Parent.ChildSize.Y / CatchPlayfield.HEIGHT));
+ Height = 1 / Scale.Y;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs
index f31dc3ef9c..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,10 +11,11 @@ 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;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Edit;
@@ -21,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;
@@ -33,10 +34,14 @@ 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)
+ {
+ MinValue = 1,
+ MaxValue = 10,
+ };
+
public CatchHitObjectComposer(CatchRuleset ruleset)
: base(ruleset)
{
@@ -51,7 +56,10 @@ namespace osu.Game.Rulesets.Catch.Edit
LayerBelowRuleset.Add(new PlayfieldBorder
{
- RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ RelativeSizeAxes = Axes.X,
+ Height = CatchPlayfield.HEIGHT,
PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners }
});
@@ -70,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();
@@ -77,8 +98,30 @@ namespace osu.Game.Rulesets.Catch.Edit
updateDistanceSnapGrid();
}
+ public override bool OnPressed(KeyBindingPressEvent e)
+ {
+ switch (e.Action)
+ {
+ // Note that right now these are hard to use as the default key bindings conflict with existing editor key bindings.
+ // In the future we will want to expose this via UI and potentially change the key bindings to be editor-specific.
+ // May be worth considering standardising "zoom" behaviour with what the timeline uses (ie. alt-wheel) but that may cause new conflicts.
+ case GlobalAction.IncreaseScrollSpeed:
+ this.TransformBindableTo(timeRangeMultiplier, timeRangeMultiplier.Value - 1, 200, Easing.OutQuint);
+ break;
+
+ case GlobalAction.DecreaseScrollSpeed:
+ this.TransformBindableTo(timeRangeMultiplier, timeRangeMultiplier.Value + 1, 200, Easing.OutQuint);
+ break;
+ }
+
+ return base.OnPressed(e);
+ }
+
protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) =>
- new DrawableCatchEditorRuleset(ruleset, beatmap, mods);
+ new DrawableCatchEditorRuleset(ruleset, beatmap, mods)
+ {
+ TimeRangeMultiplier = { BindTarget = timeRangeMultiplier, }
+ };
protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[]
{
@@ -87,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);
@@ -163,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/Edit/DrawableCatchEditorRuleset.cs b/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs
index c81afafae5..67238f66d4 100644
--- a/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs
+++ b/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs
@@ -4,6 +4,7 @@
#nullable disable
using System.Collections.Generic;
+using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods;
@@ -13,11 +14,24 @@ namespace osu.Game.Rulesets.Catch.Edit
{
public class DrawableCatchEditorRuleset : DrawableCatchRuleset
{
+ public readonly BindableDouble TimeRangeMultiplier = new BindableDouble(1);
+
public DrawableCatchEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
: base(ruleset, beatmap, mods)
{
}
+ protected override void Update()
+ {
+ base.Update();
+
+ double gamePlayTimeRange = GetTimeRange(Beatmap.Difficulty.ApproachRate);
+ float playfieldStretch = Playfield.DrawHeight / CatchPlayfield.HEIGHT;
+ TimeRange.Value = gamePlayTimeRange * TimeRangeMultiplier.Value * playfieldStretch;
+ }
+
protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.Difficulty);
+
+ public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new CatchEditorPlayfieldAdjustmentContainer();
}
}
diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
index 69ae8328e9..3f7560844c 100644
--- a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
+++ b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs
@@ -19,17 +19,20 @@ namespace osu.Game.Rulesets.Catch.Mods
{
public override LocalisableString Description => @"Use the mouse to control the catcher.";
- private DrawableRuleset drawableRuleset = null!;
+ private DrawableCatchRuleset drawableRuleset = null!;
public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset)
{
- this.drawableRuleset = drawableRuleset;
+ this.drawableRuleset = (DrawableCatchRuleset)drawableRuleset;
}
public void ApplyToPlayer(Player player)
{
if (!drawableRuleset.HasReplayLoaded.Value)
- drawableRuleset.Cursor.Add(new MouseInputHelper((CatchPlayfield)drawableRuleset.Playfield));
+ {
+ var catchPlayfield = (CatchPlayfield)drawableRuleset.Playfield;
+ catchPlayfield.CatcherArea.Add(new MouseInputHelper(catchPlayfield.CatcherArea));
+ }
}
private class MouseInputHelper : Drawable, IKeyBindingHandler, IRequireHighFrequencyMousePosition
@@ -38,9 +41,10 @@ namespace osu.Game.Rulesets.Catch.Mods
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
- public MouseInputHelper(CatchPlayfield playfield)
+ public MouseInputHelper(CatcherArea catcherArea)
{
- catcherArea = playfield.CatcherArea;
+ this.catcherArea = catcherArea;
+
RelativeSizeAxes = Axes.Both;
}
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.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
index dad22fbe69..ce000b0fad 100644
--- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
+++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs
@@ -23,6 +23,12 @@ namespace osu.Game.Rulesets.Catch.UI
///
public const float WIDTH = 512;
+ ///
+ /// The height of the playfield.
+ /// This doesn't include the catcher area.
+ ///
+ public const float HEIGHT = 384;
+
///
/// The center position of the playfield.
///
diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
index 27f7886d79..e02b915508 100644
--- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
+++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs
@@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.UI
: base(ruleset, beatmap, mods)
{
Direction.Value = ScrollingDirection.Down;
- TimeRange.Value = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450);
+ TimeRange.Value = GetTimeRange(beatmap.Difficulty.ApproachRate);
}
[BackgroundDependencyLoader]
@@ -42,6 +42,8 @@ namespace osu.Game.Rulesets.Catch.UI
KeyBindingInputManager.Add(new CatchTouchInputMapper());
}
+ protected double GetTimeRange(float approachRate) => IBeatmapDifficultyInfo.DifficultyRange(approachRate, 1800, 1200, 450);
+
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay);
protected override ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield);
diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFlashlight.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFlashlight.cs
new file mode 100644
index 0000000000..0e222fea89
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModFlashlight.cs
@@ -0,0 +1,23 @@
+// 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.Mania.Mods;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Mania.Tests.Mods
+{
+ public class TestSceneManiaModFlashlight : ModTestScene
+ {
+ protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
+
+ [TestCase(1f)]
+ [TestCase(0.5f)]
+ [TestCase(1.5f)]
+ [TestCase(3f)]
+ public void TestSizeMultiplier(float sizeMultiplier) => CreateModTest(new ModTestData { Mod = new ManiaModFlashlight { SizeMultiplier = { Value = sizeMultiplier } }, PassCondition = () => true });
+
+ [Test]
+ public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new ManiaModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
+ }
+}
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 c102678e00..1b67fc2ca9 100644
--- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs
@@ -4,14 +4,18 @@
#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;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Edit;
@@ -33,6 +37,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[Cached(typeof(IBeatSnapProvider))]
private readonly EditorBeatmap editorBeatmap;
+ [Cached]
+ private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
+
[Cached]
private readonly EditorClock editorClock;
@@ -48,6 +55,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
};
private OsuDistanceSnapGrid grid;
+ private SnappingCursorContainer cursor;
public TestSceneOsuDistanceSnapGrid()
{
@@ -84,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 }
};
});
@@ -150,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()
{
@@ -162,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 }
};
});
@@ -182,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;
@@ -210,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/TestSceneOsuModFlashlight.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs
new file mode 100644
index 0000000000..704a548c61
--- /dev/null
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFlashlight.cs
@@ -0,0 +1,25 @@
+// 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 TestSceneOsuModFlashlight : OsuModTestScene
+ {
+ [TestCase(600)]
+ [TestCase(120)]
+ [TestCase(1200)]
+ public void TestFollowDelay(double followDelay) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { FollowDelay = { Value = followDelay } }, PassCondition = () => true });
+
+ [TestCase(1f)]
+ [TestCase(0.5f)]
+ [TestCase(1.5f)]
+ [TestCase(2f)]
+ public void TestSizeMultiplier(float sizeMultiplier) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { SizeMultiplier = { Value = sizeMultiplier } }, PassCondition = () => true });
+
+ [Test]
+ public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
+ }
+}
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/Mods/TestSceneOsuModNoScope.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModNoScope.cs
index 44404ca245..da6fac3269 100644
--- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModNoScope.cs
+++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModNoScope.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Utils;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
@@ -12,6 +13,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
@@ -145,6 +147,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
private bool isBreak() => Player.IsBreakTime.Value;
- private bool cursorAlphaAlmostEquals(float alpha) => Precision.AlmostEquals(Player.DrawableRuleset.Cursor.Alpha, alpha, 0.1f);
+ private OsuPlayfield playfield => (OsuPlayfield)Player.DrawableRuleset.Playfield;
+
+ private bool cursorAlphaAlmostEquals(float alpha) =>
+ Precision.AlmostEquals(playfield.Cursor.AsNonNull().Alpha, alpha, 0.1f) &&
+ Precision.AlmostEquals(playfield.Smoke.Alpha, alpha, 0.1f);
}
}
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.Tests/TestSceneOsuFlashlight.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuFlashlight.cs
deleted file mode 100644
index e0d1646cb0..0000000000
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuFlashlight.cs
+++ /dev/null
@@ -1,21 +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.Rulesets.Mods;
-using osu.Game.Rulesets.Osu.Mods;
-using osu.Game.Tests.Visual;
-
-namespace osu.Game.Rulesets.Osu.Tests
-{
- public class TestSceneOsuFlashlight : TestSceneOsuPlayer
- {
- protected override TestPlayer CreatePlayer(Ruleset ruleset)
- {
- SelectedMods.Value = new Mod[] { new OsuModAutoplay(), new OsuModFlashlight(), };
-
- return base.CreatePlayer(ruleset);
- }
- }
-}
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs
index 0169627867..728aa27da2 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs
@@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
+using osu.Game.Rulesets.Osu.Skinning.Legacy;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
using osuTK;
@@ -68,10 +69,8 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("create slider", () =>
{
- var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.CreateInfo());
- tintingSkin.Configuration.ConfigDictionary["AllowSliderBallTint"] = "1";
-
- var provider = Ruleset.Value.CreateInstance().CreateSkinTransformer(tintingSkin, Beatmap.Value.Beatmap);
+ var skin = skinManager.GetSkin(DefaultLegacySkin.CreateInfo());
+ var provider = Ruleset.Value.CreateInstance().CreateSkinTransformer(skin, Beatmap.Value.Beatmap);
Child = new SkinProvidingContainer(provider)
{
@@ -92,10 +91,10 @@ namespace osu.Game.Rulesets.Osu.Tests
});
AddStep("set accent white", () => dho.AccentColour.Value = Color4.White);
- AddAssert("ball is white", () => dho.ChildrenOfType().Single().AccentColour == Color4.White);
+ AddAssert("ball is white", () => dho.ChildrenOfType().Single().BallColour == Color4.White);
AddStep("set accent red", () => dho.AccentColour.Value = Color4.Red);
- AddAssert("ball is red", () => dho.ChildrenOfType().Single().AccentColour == Color4.Red);
+ AddAssert("ball is red", () => dho.ChildrenOfType().Single().BallColour == Color4.Red);
}
private Slider prepareObject(Slider slider)
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 60896b17bf..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 })
});
@@ -60,6 +61,9 @@ namespace osu.Game.Rulesets.Osu.Edit
[BackgroundDependencyLoader]
private void load()
{
+ // Give a bit of breathing room around the playfield content.
+ PlayfieldContentContainer.Padding = new MarginPadding(10);
+
LayerBelowRuleset.AddRange(new Drawable[]
{
distanceSnapGridContainer = new Container
@@ -77,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();
@@ -109,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();
@@ -129,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)
@@ -199,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)
@@ -226,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/OsuModMagnetised.cs b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
index fbde9e0491..38d90eb121 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModMagnetised.cs
@@ -3,6 +3,7 @@
using System;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Timing;
@@ -46,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public void Update(Playfield playfield)
{
- var cursorPos = playfield.Cursor.ActiveCursor.DrawPosition;
+ var cursorPos = playfield.Cursor.AsNonNull().ActiveCursor.DrawPosition;
foreach (var drawable in playfield.HitObjectContainer.AliveObjects)
{
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs
index 2f84c30581..d1bbae8e1a 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using System.Diagnostics;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
@@ -9,6 +10,7 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using osu.Game.Utils;
@@ -33,9 +35,15 @@ namespace osu.Game.Rulesets.Osu.Mods
public void Update(Playfield playfield)
{
- bool shouldAlwaysShowCursor = IsBreakTime.Value || spinnerPeriods.IsInAny(playfield.Clock.CurrentTime);
+ var osuPlayfield = (OsuPlayfield)playfield;
+ Debug.Assert(osuPlayfield.Cursor != null);
+
+ bool shouldAlwaysShowCursor = IsBreakTime.Value || spinnerPeriods.IsInAny(osuPlayfield.Clock.CurrentTime);
float targetAlpha = shouldAlwaysShowCursor ? 1 : ComboBasedAlpha;
- playfield.Cursor.Alpha = (float)Interpolation.Lerp(playfield.Cursor.Alpha, targetAlpha, Math.Clamp(playfield.Time.Elapsed / TRANSITION_DURATION, 0, 1));
+ float currentAlpha = (float)Interpolation.Lerp(osuPlayfield.Cursor.Alpha, targetAlpha, Math.Clamp(osuPlayfield.Time.Elapsed / TRANSITION_DURATION, 0, 1));
+
+ osuPlayfield.Cursor.Alpha = currentAlpha;
+ osuPlayfield.Smoke.Alpha = currentAlpha;
}
}
}
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/OsuModRepel.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs
index 911363a27e..31a6b69d6b 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs
@@ -3,6 +3,7 @@
using System;
using osu.Framework.Bindables;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Localisation;
using osu.Framework.Timing;
using osu.Framework.Utils;
@@ -45,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public void Update(Playfield playfield)
{
- var cursorPos = playfield.Cursor.ActiveCursor.DrawPosition;
+ var cursorPos = playfield.Cursor.AsNonNull().ActiveCursor.DrawPosition;
foreach (var drawable in playfield.HitObjectContainer.AliveObjects)
{
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/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
index d58a435728..785d15c15b 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs
@@ -14,12 +14,10 @@ using osu.Game.Audio;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
-using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osuTK;
-using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@@ -106,7 +104,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
foreach (var drawableHitObject in NestedHitObjects)
drawableHitObject.AccentColour.Value = colour.NewValue;
- updateBallTint();
}, true);
Tracking.BindValueChanged(updateSlidingSample);
@@ -257,22 +254,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
SliderBody?.RecyclePath();
}
- protected override void ApplySkin(ISkinSource skin, bool allowFallback)
- {
- base.ApplySkin(skin, allowFallback);
-
- updateBallTint();
- }
-
- private void updateBallTint()
- {
- if (CurrentSkin == null)
- return;
-
- bool allowBallTint = CurrentSkin.GetConfig(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false;
- Ball.AccentColour = allowBallTint ? AccentColour.Value : Color4.White;
- }
-
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
if (userTriggered || Time.Current < HitObject.EndTime)
@@ -331,7 +312,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.UpdateHitStateTransforms(state);
- const float fade_out_time = 450;
+ const float fade_out_time = 240;
switch (state)
{
@@ -341,7 +322,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
break;
}
- this.FadeOut(fade_out_time, Easing.OutQuint).Expire();
+ this.FadeOut(fade_out_time).Expire();
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => SliderBody?.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos);
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs
index a2fe623897..9966ad3a90 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderBall.cs
@@ -11,28 +11,20 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Events;
-using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Skinning;
using osuTK;
-using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
- public class DrawableSliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition, IHasAccentColour
+ public class DrawableSliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition
{
public const float FOLLOW_AREA = 2.4f;
public Func GetInitialHitAction;
- public Color4 AccentColour
- {
- get => ball.Colour;
- set => ball.Colour = value;
- }
-
private Drawable followCircleReceptor;
private DrawableSlider drawableSlider;
private Drawable ball;
@@ -87,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/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index 53f4d21975..6ae9d5bc34 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
///
public readonly IBindable SpinsPerMinute = new BindableDouble();
- private const double fade_out_duration = 160;
+ private const double fade_out_duration = 240;
public DrawableSpinner()
: this(null)
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/ArgonJudgementPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs
index b08b7b4e85..bb68c7298f 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonJudgementPiece.cs
@@ -75,6 +75,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
default:
JudgementText
+ .FadeInFromZero(300, Easing.OutQuint)
.ScaleTo(Vector2.One)
.ScaleTo(new Vector2(1.2f), 1800, Easing.OutQuint);
break;
@@ -96,7 +97,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
ringExplosion?.PlayAnimation();
}
- public Drawable? GetAboveHitObjectsProxiedContent() => null;
+ public Drawable? GetAboveHitObjectsProxiedContent() => JudgementText.CreateProxy();
private class RingExplosion : CompositeDrawable
{
diff --git a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs
index ffdcba3cdb..4ac71e4225 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Argon/ArgonMainCirclePiece.cs
@@ -108,20 +108,29 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
base.LoadComplete();
- accentColour.BindValueChanged(colour =>
- {
- outerFill.Colour = innerFill.Colour = colour.NewValue.Darken(4);
- outerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.1f));
- innerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue.Darken(0.5f), colour.NewValue.Darken(0.6f));
- flash.Colour = colour.NewValue;
- }, true);
-
indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true);
+ accentColour.BindValueChanged(colour =>
+ {
+ // A colour transform is applied.
+ // Without removing transforms first, when it is rewound it may apply an old colour.
+ outerGradient.ClearTransforms(targetMember: nameof(Colour));
+ outerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.1f));
+
+ outerFill.Colour = innerFill.Colour = colour.NewValue.Darken(4);
+ innerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue.Darken(0.5f), colour.NewValue.Darken(0.6f));
+ flash.Colour = colour.NewValue;
+
+ // 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;
- updateStateTransforms(drawableObject, drawableObject.State.Value);
}
+ private void updateStateTransforms() => updateStateTransforms(drawableObject, drawableObject.State.Value);
+
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
@@ -166,18 +175,14 @@ 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()
.FadeOut(flash_in_duration);
}
- // The flash layer starts white to give the wanted brightness, but is almost immediately
- // recoloured to the accent colour. This would more correctly be done with two layers (one for the initial flash)
- // but works well enough with the colour fade.
flash.FadeTo(1, flash_in_duration, Easing.OutQuint);
- flash.FlashColour(accentColour.Value, fade_out_time, Easing.OutQuint);
this.FadeOut(fade_out_time, Easing.OutQuad);
break;
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs
index 1b2ab82044..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,
@@ -134,10 +134,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
switch (state)
{
case ArmedState.Hit:
- CircleSprite.FadeOut(legacy_fade_duration, Easing.Out);
+ CircleSprite.FadeOut(legacy_fade_duration);
CircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
- OverlaySprite.FadeOut(legacy_fade_duration, Easing.Out);
+ OverlaySprite.FadeOut(legacy_fade_duration);
OverlaySprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
if (hasNumber)
@@ -146,11 +146,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
if (legacyVersion >= 2.0m)
// legacy skins of version 2.0 and newer only apply very short fade out to the number piece.
- hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out);
+ hitCircleText.FadeOut(legacy_fade_duration / 4);
else
{
// old skins scale and fade it normally along other pieces.
- hitCircleText.FadeOut(legacy_fade_duration, Easing.Out);
+ hitCircleText.FadeOut(legacy_fade_duration);
hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
index 22944becf3..71c3e4c9f0 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs
@@ -107,8 +107,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
this.FadeOut();
- using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2))
- this.FadeInFromZero(spinner.TimeFadeIn / 2);
+ using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn))
+ this.FadeInFromZero(spinner.TimeFadeIn);
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
{
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs
index 414879f42d..60d71ae843 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
+using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
@@ -21,6 +22,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
[Resolved(canBeNull: true)]
private DrawableHitObject? parentObject { get; set; }
+ public Color4 BallColour => animationContent.Colour;
+
private Sprite layerNd = null!;
private Sprite layerSpec = null!;
@@ -61,6 +64,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
};
}
+ private readonly IBindable accentColour = new Bindable();
+
protected override void LoadComplete()
{
base.LoadComplete();
@@ -69,6 +74,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
parentObject.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(parentObject, parentObject.State.Value);
+
+ if (skin.GetConfig(SkinConfiguration.LegacySetting.AllowSliderBallTint)?.Value == true)
+ {
+ accentColour.BindTo(parentObject.AccentColour);
+ accentColour.BindValueChanged(a => animationContent.Colour = a.NewValue, true);
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
index 004222ad7a..a817e5f2b7 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs
@@ -65,6 +65,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
spin = new Sprite
{
+ Alpha = 0,
Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-spin"),
@@ -82,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
},
bonusCounter = new LegacySpriteText(LegacyFont.Score)
{
- Alpha = 0f,
+ Alpha = 0,
Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
Scale = new Vector2(SPRITE_SCALE),
@@ -179,6 +180,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
spmCounter.MoveToOffset(new Vector2(0, -spm_hide_offset), d.HitObject.TimeFadeIn, Easing.Out);
}
+ using (BeginAbsoluteSequence(d.HitObject.StartTime - d.HitObject.TimeFadeIn / 2))
+ spin.FadeInFromZero(d.HitObject.TimeFadeIn / 2);
+
using (BeginAbsoluteSequence(d.HitObject.StartTime))
ApproachCircle?.ScaleTo(SPRITE_SCALE * 0.1f, d.HitObject.Duration);
diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
index 306a1e38b9..1c0a62454b 100644
--- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs
@@ -9,7 +9,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
SliderBorderSize,
SliderPathRadius,
- AllowSliderBallTint,
CursorCentre,
CursorExpand,
CursorRotate,
diff --git a/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
index 6c998e244c..46c8e7c02a 100644
--- a/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
+using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@@ -14,6 +15,7 @@ using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Textures;
using osu.Framework.Utils;
+using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
@@ -21,8 +23,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
public abstract class SmokeSegment : Drawable, ITexturedShaderDrawable
{
- private const int max_point_count = 18_000;
-
// fade anim values
private const double initial_fade_out_duration = 4000;
@@ -84,12 +84,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
totalDistance = pointInterval;
}
- private Vector2 nextPointDirection()
- {
- float angle = RNG.NextSingle(0, 2 * MathF.PI);
- return new Vector2(MathF.Sin(angle), -MathF.Cos(angle));
- }
-
public void AddPosition(Vector2 position, double time)
{
lastPosition ??= position;
@@ -106,33 +100,27 @@ namespace osu.Game.Rulesets.Osu.Skinning
Vector2 pointPos = (pointInterval - (totalDistance - delta)) * increment + (Vector2)lastPosition;
increment *= pointInterval;
- if (SmokePoints.Count > 0 && SmokePoints[^1].Time > time)
- {
- int index = ~SmokePoints.BinarySearch(new SmokePoint { Time = time }, new SmokePoint.UpperBoundComparer());
- SmokePoints.RemoveRange(index, SmokePoints.Count - index);
- }
-
totalDistance %= pointInterval;
- for (int i = 0; i < count; i++)
+ if (SmokePoints.Count == 0 || SmokePoints[^1].Time <= time)
{
- SmokePoints.Add(new SmokePoint
+ for (int i = 0; i < count; i++)
{
- Position = pointPos,
- Time = time,
- Direction = nextPointDirection(),
- });
+ SmokePoints.Add(new SmokePoint
+ {
+ Position = pointPos,
+ Time = time,
+ Angle = RNG.NextSingle(0, 2 * MathF.PI),
+ });
- pointPos += increment;
+ pointPos += increment;
+ }
}
Invalidate(Invalidation.DrawNode);
}
lastPosition = position;
-
- if (SmokePoints.Count >= max_point_count)
- FinishDrawing(time);
}
public void FinishDrawing(double time)
@@ -156,7 +144,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
public Vector2 Position;
public double Time;
- public Vector2 Direction;
+ public float Angle;
public struct UpperBoundComparer : IComparer
{
@@ -170,6 +158,17 @@ namespace osu.Game.Rulesets.Osu.Skinning
return x.Time > target.Time ? 1 : -1;
}
}
+
+ public struct LowerBoundComparer : IComparer
+ {
+ public int Compare(SmokePoint x, SmokePoint target)
+ {
+ // Similar logic as UpperBoundComparer, except returned index will always be
+ // the first element larger or equal
+
+ return x.Time < target.Time ? -1 : 1;
+ }
+ }
}
protected class SmokeDrawNode : TexturedShaderDrawNode
@@ -185,17 +184,17 @@ namespace osu.Game.Rulesets.Osu.Skinning
private float radius;
private Vector2 drawSize;
private Texture? texture;
+ private int rotationSeed;
+ private int firstVisiblePointIndex;
// anim calculation vars (color, scale, direction)
private double initialFadeOutDurationTrunc;
- private double firstVisiblePointTime;
+ private double firstVisiblePointTimeAfterSmokeEnded;
private double initialFadeOutTime;
private double reFadeInTime;
private double finalFadeOutTime;
- private Random rotationRNG = new Random();
-
public SmokeDrawNode(ITexturedShaderDrawable source)
: base(source)
{
@@ -205,9 +204,6 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
base.ApplyState();
- points.Clear();
- points.AddRange(Source.SmokePoints);
-
radius = Source.radius;
drawSize = Source.DrawSize;
texture = Source.Texture;
@@ -216,14 +212,21 @@ namespace osu.Game.Rulesets.Osu.Skinning
SmokeEndTime = Source.smokeEndTime;
CurrentTime = Source.Clock.CurrentTime;
- rotationRNG = new Random(Source.rotationSeed);
+ rotationSeed = Source.rotationSeed;
initialFadeOutDurationTrunc = Math.Min(initial_fade_out_duration, SmokeEndTime - SmokeStartTime);
- firstVisiblePointTime = SmokeEndTime - initialFadeOutDurationTrunc;
+ firstVisiblePointTimeAfterSmokeEnded = SmokeEndTime - initialFadeOutDurationTrunc;
- initialFadeOutTime = CurrentTime;
- reFadeInTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTime * (1 - 1 / re_fade_in_speed);
- finalFadeOutTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTime * (1 - 1 / final_fade_out_speed);
+ initialFadeOutTime = Math.Min(CurrentTime, SmokeEndTime);
+ reFadeInTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTimeAfterSmokeEnded * (1 - 1 / re_fade_in_speed);
+ finalFadeOutTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTimeAfterSmokeEnded * (1 - 1 / final_fade_out_speed);
+
+ double firstVisiblePointTime = Math.Min(SmokeEndTime, CurrentTime) - initialFadeOutDurationTrunc;
+ firstVisiblePointIndex = ~Source.SmokePoints.BinarySearch(new SmokePoint { Time = firstVisiblePointTime }, new SmokePoint.LowerBoundComparer());
+ int futurePointIndex = ~Source.SmokePoints.BinarySearch(new SmokePoint { Time = CurrentTime }, new SmokePoint.UpperBoundComparer());
+
+ points.Clear();
+ points.AddRange(Source.SmokePoints.Skip(firstVisiblePointIndex).Take(futurePointIndex - firstVisiblePointIndex));
}
public sealed override void Draw(IRenderer renderer)
@@ -233,7 +236,14 @@ namespace osu.Game.Rulesets.Osu.Skinning
if (points.Count == 0)
return;
- quadBatch ??= renderer.CreateQuadBatch(max_point_count / 10, 10);
+ quadBatch ??= renderer.CreateQuadBatch(200, 4);
+
+ if (points.Count > quadBatch.Size && quadBatch.Size != IRenderer.MAX_QUADS)
+ {
+ int batchSize = Math.Min(quadBatch.Size * 2, IRenderer.MAX_QUADS);
+ quadBatch = renderer.CreateQuadBatch(batchSize, 4);
+ }
+
texture ??= renderer.WhitePixel;
RectangleF textureRect = texture.GetTextureRect();
@@ -245,8 +255,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
shader.Bind();
texture.Bind();
- foreach (var point in points)
- drawPointQuad(point, textureRect);
+ for (int i = 0; i < points.Count; i++)
+ drawPointQuad(points[i], textureRect, i + firstVisiblePointIndex);
shader.Unbind();
renderer.PopLocalMatrix();
@@ -260,30 +270,34 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
var color = Color4.White;
- double timeDoingInitialFadeOut = Math.Min(initialFadeOutTime, SmokeEndTime) - point.Time;
+ double timeDoingFinalFadeOut = finalFadeOutTime - point.Time / final_fade_out_speed;
- if (timeDoingInitialFadeOut > 0)
+ if (timeDoingFinalFadeOut > 0 && point.Time >= firstVisiblePointTimeAfterSmokeEnded)
{
- float fraction = Math.Clamp((float)(timeDoingInitialFadeOut / initial_fade_out_duration), 0, 1);
- color.A = (1 - fraction) * initial_alpha;
+ float fraction = Math.Clamp((float)(timeDoingFinalFadeOut / final_fade_out_duration), 0, 1);
+ fraction = MathF.Pow(fraction, 5);
+ color.A = (1 - fraction) * re_fade_in_alpha;
}
-
- if (color.A > 0)
+ else
{
- double timeDoingReFadeIn = reFadeInTime - point.Time / re_fade_in_speed;
- double timeDoingFinalFadeOut = finalFadeOutTime - point.Time / final_fade_out_speed;
+ double timeDoingInitialFadeOut = initialFadeOutTime - point.Time;
- if (timeDoingFinalFadeOut > 0)
+ if (timeDoingInitialFadeOut > 0)
{
- float fraction = Math.Clamp((float)(timeDoingFinalFadeOut / final_fade_out_duration), 0, 1);
- fraction = MathF.Pow(fraction, 5);
- color.A = (1 - fraction) * re_fade_in_alpha;
+ float fraction = Math.Clamp((float)(timeDoingInitialFadeOut / initial_fade_out_duration), 0, 1);
+ color.A = (1 - fraction) * initial_alpha;
}
- else if (timeDoingReFadeIn > 0)
+
+ if (point.Time > firstVisiblePointTimeAfterSmokeEnded)
{
- float fraction = Math.Clamp((float)(timeDoingReFadeIn / re_fade_in_duration), 0, 1);
- fraction = 1 - MathF.Pow(1 - fraction, 5);
- color.A = fraction * (re_fade_in_alpha - color.A) + color.A;
+ double timeDoingReFadeIn = reFadeInTime - point.Time / re_fade_in_speed;
+
+ if (timeDoingReFadeIn > 0)
+ {
+ float fraction = Math.Clamp((float)(timeDoingReFadeIn / re_fade_in_duration), 0, 1);
+ fraction = 1 - MathF.Pow(1 - fraction, 5);
+ color.A = fraction * (re_fade_in_alpha - color.A) + color.A;
+ }
}
}
@@ -298,33 +312,33 @@ namespace osu.Game.Rulesets.Osu.Skinning
return fraction * (final_scale - initial_scale) + initial_scale;
}
- protected virtual Vector2 PointDirection(SmokePoint point)
+ protected virtual Vector2 PointDirection(SmokePoint point, int index)
{
- float initialAngle = MathF.Atan2(point.Direction.Y, point.Direction.X);
- float finalAngle = initialAngle + nextRotation();
-
double timeDoingRotation = CurrentTime - point.Time;
float fraction = Math.Clamp((float)(timeDoingRotation / rotation_duration), 0, 1);
fraction = 1 - MathF.Pow(1 - fraction, 5);
- float angle = fraction * (finalAngle - initialAngle) + initialAngle;
+ float angle = fraction * getRotation(index) + point.Angle;
return new Vector2(MathF.Sin(angle), -MathF.Cos(angle));
}
- private float nextRotation() => max_rotation * ((float)rotationRNG.NextDouble() * 2 - 1);
+ private float getRotation(int index) => max_rotation * (StatelessRNG.NextSingle(rotationSeed, index) * 2 - 1);
- private void drawPointQuad(SmokePoint point, RectangleF textureRect)
+ private void drawPointQuad(SmokePoint point, RectangleF textureRect, int index)
{
Debug.Assert(quadBatch != null);
var colour = PointColour(point);
- float scale = PointScale(point);
- var dir = PointDirection(point);
- var ortho = dir.PerpendicularLeft;
-
- if (colour.A == 0 || scale == 0)
+ if (colour.A == 0)
return;
+ float scale = PointScale(point);
+ if (scale == 0)
+ return;
+
+ var dir = PointDirection(point, index);
+ var ortho = dir.PerpendicularLeft;
+
var localTopLeft = point.Position + (radius * scale * (-ortho - dir));
var localTopRight = point.Position + (radius * scale * (-ortho + dir));
var localBotLeft = point.Position + (radius * scale * (ortho - dir));
diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
index 2e67e91460..e9a6c84c0b 100644
--- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
+++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs
@@ -36,6 +36,7 @@ namespace osu.Game.Rulesets.Osu.UI
private readonly ProxyContainer spinnerProxies;
private readonly JudgementContainer judgementLayer;
+ public SmokeContainer Smoke { get; }
public FollowPointRenderer FollowPoints { get; }
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
@@ -54,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.UI
InternalChildren = new Drawable[]
{
playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },
- new SmokeContainer { RelativeSizeAxes = Axes.Both },
+ Smoke = new SmokeContainer { RelativeSizeAxes = Axes.Both },
spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both },
FollowPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both },
judgementLayer = new JudgementContainer { RelativeSizeAxes = Axes.Both },
diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModFlashlight.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModFlashlight.cs
new file mode 100644
index 0000000000..d55ce17e6c
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModFlashlight.cs
@@ -0,0 +1,56 @@
+// 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
+{
+ public class TestSceneTaikoModFlashlight : TaikoModTestScene
+ {
+ [TestCase(1f)]
+ [TestCase(0.5f)]
+ [TestCase(1.25f)]
+ [TestCase(1.5f)]
+ public void TestSizeMultiplier(float sizeMultiplier) => CreateModTest(new ModTestData { Mod = new TaikoModFlashlight { SizeMultiplier = { Value = sizeMultiplier } }, PassCondition = () => true });
+
+ [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/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
index 5aadd6f56a..917434ae22 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs
@@ -244,7 +244,10 @@ namespace osu.Game.Tests.Visual.Background
public void TestResumeFromPlayer()
{
performFullSetup();
- AddStep("Move mouse to Visual Settings", () => InputManager.MoveMouseTo(playerLoader.VisualSettingsPos));
+ AddStep("Move mouse to Visual Settings location", () => InputManager.MoveMouseTo(playerLoader.ScreenSpaceDrawQuad.TopRight
+ + new Vector2(-playerLoader.VisualSettingsPos.ScreenSpaceDrawQuad.Width,
+ playerLoader.VisualSettingsPos.ScreenSpaceDrawQuad.Height / 2
+ )));
AddStep("Resume PlayerLoader", () => player.Restart());
AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddStep("Move mouse to center of screen", () => InputManager.MoveMouseTo(playerLoader.ScreenPos));
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/TestSceneEditorComposeRadioButtons.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs
index 56435c69a4..6a69347651 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs
@@ -4,8 +4,10 @@
#nullable disable
using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
+using osu.Game.Overlays;
using osu.Game.Screens.Edit.Components.RadioButtons;
namespace osu.Game.Tests.Visual.Editing
@@ -13,6 +15,9 @@ namespace osu.Game.Tests.Visual.Editing
[TestFixture]
public class TestSceneEditorComposeRadioButtons : OsuTestScene
{
+ [Cached]
+ private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
+
public TestSceneEditorComposeRadioButtons()
{
EditorRadioButtonCollection collection;
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs
index adb495f3d3..1c87eb49c9 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs
@@ -148,10 +148,6 @@ namespace osu.Game.Tests.Visual.Editing
});
AddAssert("no circles placed", () => editorBeatmap.HitObjects.Count == 0);
-
- AddStep("place circle", () => InputManager.Click(MouseButton.Left));
-
- AddAssert("circle placed", () => editorBeatmap.HitObjects.Count == 1);
}
[Test]
@@ -165,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/Editing/TestSceneZoomableScrollContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs
index 1858aee76b..89c5b9b23b 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs
@@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.Editing
}
}
},
- new MenuCursor()
+ new MenuCursorContainer()
};
scrollContainer.Add(innerBox = new Box
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/Gameplay/TestSceneScrollingHitObjects.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
index 156a1ee34a..6d036f8e9b 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
@@ -214,7 +214,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private void addControlPoints(IList controlPoints, double sequenceStartTime)
{
- controlPoints.ForEach(point => point.StartTime += sequenceStartTime);
+ controlPoints.ForEach(point => point.Time += sequenceStartTime);
scrollContainers.ForEach(container =>
{
@@ -224,7 +224,7 @@ namespace osu.Game.Tests.Visual.Gameplay
foreach (var playfield in playfields)
{
foreach (var controlPoint in controlPoints)
- playfield.Add(createDrawablePoint(playfield, controlPoint.StartTime));
+ playfield.Add(createDrawablePoint(playfield, controlPoint.Time));
}
}
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs
index f31261dc1f..63677ce378 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerQueueList.cs
@@ -97,14 +97,23 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
[Test]
- public void TestCurrentItemDoesNotHaveDeleteButton()
+ public void TestSingleItemDoesNotHaveDeleteButton()
+ {
+ AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
+ AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode.Value == QueueMode.AllPlayers);
+
+ assertDeleteButtonVisibility(0, false);
+ }
+
+ [Test]
+ public void TestCurrentItemHasDeleteButtonIfNotSingle()
{
AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode.Value == QueueMode.AllPlayers);
addPlaylistItem(() => API.LocalUser.Value.OnlineID);
- assertDeleteButtonVisibility(0, false);
+ assertDeleteButtonVisibility(0, true);
assertDeleteButtonVisibility(1, true);
AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely());
diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs
index cf62c73ad4..6070b1456f 100644
--- a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs
@@ -25,6 +25,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Menu;
using osu.Game.Skinning;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.Navigation
{
@@ -79,6 +80,16 @@ namespace osu.Game.Tests.Visual.Navigation
[Resolved]
private OsuGameBase gameBase { get; set; }
+ [Test]
+ public void TestCursorHidesWhenIdle()
+ {
+ AddStep("click mouse", () => InputManager.Click(MouseButton.Left));
+ AddUntilStep("wait until idle", () => Game.IsIdle.Value);
+ AddUntilStep("menu cursor hidden", () => Game.GlobalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 0);
+ AddStep("click mouse", () => InputManager.Click(MouseButton.Left));
+ AddUntilStep("menu cursor shown", () => Game.GlobalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 1);
+ }
+
[Test]
public void TestNullRulesetHandled()
{
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/SongSelect/TestSceneUpdateBeatmapSetButton.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs
index f76f050546..96cfbe4dd1 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
+using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
@@ -150,6 +151,7 @@ namespace osu.Game.Tests.Visual.SongSelect
public void TestUpdateLocalBeatmap()
{
DialogOverlay dialogOverlay = null!;
+ UpdateBeatmapSetButton? updateButton = null;
AddStep("create carousel with dialog overlay", () =>
{
@@ -176,7 +178,8 @@ namespace osu.Game.Tests.Visual.SongSelect
carousel.UpdateBeatmapSet(testBeatmapSetInfo);
});
- AddStep("click button", () => getUpdateButton()?.TriggerClick());
+ AddUntilStep("wait for update button", () => (updateButton = getUpdateButton()) != null);
+ AddStep("click button", () => updateButton.AsNonNull().TriggerClick());
AddAssert("dialog displayed", () => dialogOverlay.CurrentDialog is UpdateLocalConfirmationDialog);
AddStep("click confirmation", () =>
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs
index bcbe146456..75c47f0b1b 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCursors.cs
@@ -15,6 +15,7 @@ using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
@@ -81,25 +82,24 @@ namespace osu.Game.Tests.Visual.UserInterface
};
AddToggleStep("Smooth transitions", b => cursorBoxes.ForEach(box => box.SmoothTransition = b));
-
- testUserCursor();
- testLocalCursor();
- testUserCursorOverride();
- testMultipleLocalCursors();
}
+ [SetUp]
+ public void SetUp() => Schedule(moveOut);
+
///
/// -- Green Box --
/// Tests whether hovering in and out of a drawable that provides the user cursor (green)
/// results in the correct visibility state for that cursor.
///
- private void testUserCursor()
+ [Test]
+ public void TestUserCursor()
{
AddStep("Move to green area", () => InputManager.MoveMouseTo(cursorBoxes[0]));
- AddAssert("Check green cursor visible", () => checkVisible(cursorBoxes[0].MenuCursor));
- AddAssert("Check green cursor at mouse", () => checkAtMouse(cursorBoxes[0].MenuCursor));
+ AddAssert("Check green cursor visible", () => checkVisible(cursorBoxes[0].Cursor));
+ AddAssert("Check green cursor at mouse", () => checkAtMouse(cursorBoxes[0].Cursor));
AddStep("Move out", moveOut);
- AddAssert("Check green cursor invisible", () => !checkVisible(cursorBoxes[0].MenuCursor));
+ AddAssert("Check green cursor invisible", () => !checkVisible(cursorBoxes[0].Cursor));
AddAssert("Check global cursor visible", () => checkVisible(globalCursorDisplay.MenuCursor));
}
@@ -108,15 +108,16 @@ namespace osu.Game.Tests.Visual.UserInterface
/// Tests whether hovering in and out of a drawable that provides a local cursor (purple)
/// results in the correct visibility and state for that cursor.
///
- private void testLocalCursor()
+ [Test]
+ public void TestLocalCursor()
{
AddStep("Move to purple area", () => InputManager.MoveMouseTo(cursorBoxes[3]));
- AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].MenuCursor));
- AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].MenuCursor));
+ AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
+ AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].Cursor));
AddAssert("Check global cursor visible", () => checkVisible(globalCursorDisplay.MenuCursor));
AddAssert("Check global cursor at mouse", () => checkAtMouse(globalCursorDisplay.MenuCursor));
AddStep("Move out", moveOut);
- AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].MenuCursor));
+ AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
AddAssert("Check global cursor visible", () => checkVisible(globalCursorDisplay.MenuCursor));
}
@@ -125,47 +126,98 @@ namespace osu.Game.Tests.Visual.UserInterface
/// Tests whether overriding a user cursor (green) with another user cursor (blue)
/// results in the correct visibility and states for the cursors.
///
- private void testUserCursorOverride()
+ [Test]
+ public void TestUserCursorOverride()
{
AddStep("Move to blue-green boundary", () => InputManager.MoveMouseTo(cursorBoxes[1].ScreenSpaceDrawQuad.BottomRight - new Vector2(10)));
- AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].MenuCursor));
- AddAssert("Check green cursor invisible", () => !checkVisible(cursorBoxes[0].MenuCursor));
- AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].MenuCursor));
+ AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].Cursor));
+ AddAssert("Check green cursor invisible", () => !checkVisible(cursorBoxes[0].Cursor));
+ AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].Cursor));
AddStep("Move out", moveOut);
- AddAssert("Check blue cursor not visible", () => !checkVisible(cursorBoxes[1].MenuCursor));
- AddAssert("Check green cursor not visible", () => !checkVisible(cursorBoxes[0].MenuCursor));
+ AddAssert("Check blue cursor not visible", () => !checkVisible(cursorBoxes[1].Cursor));
+ AddAssert("Check green cursor not visible", () => !checkVisible(cursorBoxes[0].Cursor));
}
///
/// -- Yellow-Purple Box Boundary --
/// Tests whether multiple local cursors (purple + yellow) may be visible and at the mouse position at the same time.
///
- private void testMultipleLocalCursors()
+ [Test]
+ public void TestMultipleLocalCursors()
{
AddStep("Move to yellow-purple boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.BottomRight - new Vector2(10)));
- AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].MenuCursor));
- AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].MenuCursor));
- AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor));
- AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].MenuCursor));
+ AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
+ AddAssert("Check purple cursor at mouse", () => checkAtMouse(cursorBoxes[3].Cursor));
+ AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor));
+ AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].Cursor));
AddStep("Move out", moveOut);
- AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].MenuCursor));
- AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor));
+ AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
+ AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor));
}
///
/// -- Yellow-Blue Box Boundary --
/// Tests whether a local cursor (yellow) may be displayed along with a user cursor override (blue).
///
- private void testUserOverrideWithLocal()
+ [Test]
+ public void TestUserOverrideWithLocal()
{
- AddStep("Move to yellow-blue boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.TopRight - new Vector2(10)));
- AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].MenuCursor));
- AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].MenuCursor));
- AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor));
- AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].MenuCursor));
+ AddStep("Move to yellow-blue boundary", () => InputManager.MoveMouseTo(cursorBoxes[5].ScreenSpaceDrawQuad.TopRight - new Vector2(10, 0)));
+ AddAssert("Check blue cursor visible", () => checkVisible(cursorBoxes[1].Cursor));
+ AddAssert("Check blue cursor at mouse", () => checkAtMouse(cursorBoxes[1].Cursor));
+ AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor));
+ AddAssert("Check yellow cursor at mouse", () => checkAtMouse(cursorBoxes[5].Cursor));
AddStep("Move out", moveOut);
- AddAssert("Check blue cursor invisible", () => !checkVisible(cursorBoxes[1].MenuCursor));
- AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].MenuCursor));
+ AddAssert("Check blue cursor invisible", () => !checkVisible(cursorBoxes[1].Cursor));
+ AddAssert("Check yellow cursor visible", () => checkVisible(cursorBoxes[5].Cursor));
+ }
+
+ ///
+ /// Ensures non-mouse input hides global cursor on a "local cursor" area (which doesn't hide global cursor).
+ ///
+ [Test]
+ public void TestKeyboardLocalCursor([Values] bool clickToShow)
+ {
+ AddStep("Enable cursor hiding", () => globalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = true);
+ AddStep("Move to purple area", () => InputManager.MoveMouseTo(cursorBoxes[3].ScreenSpaceDrawQuad.Centre + new Vector2(10, 0)));
+ AddAssert("Check purple cursor visible", () => checkVisible(cursorBoxes[3].Cursor));
+ AddAssert("Check global cursor alpha is 1", () => globalCursorDisplay.MenuCursor.Alpha == 1);
+
+ AddStep("Press key", () => InputManager.Key(Key.A));
+ AddAssert("Check purple cursor still visible", () => checkVisible(cursorBoxes[3].Cursor));
+ AddUntilStep("Check global cursor alpha is 0", () => globalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 0);
+
+ if (clickToShow)
+ AddStep("Click mouse", () => InputManager.Click(MouseButton.Left));
+ else
+ AddStep("Move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + Vector2.One));
+
+ AddAssert("Check purple cursor still visible", () => checkVisible(cursorBoxes[3].Cursor));
+ AddUntilStep("Check global cursor alpha is 1", () => globalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 1);
+ }
+
+ ///
+ /// Ensures mouse input after non-mouse input doesn't show global cursor on a "user cursor" area (which hides global cursor).
+ ///
+ [Test]
+ public void TestKeyboardUserCursor([Values] bool clickToShow)
+ {
+ AddStep("Enable cursor hiding", () => globalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = true);
+ AddStep("Move to green area", () => InputManager.MoveMouseTo(cursorBoxes[0]));
+ AddAssert("Check green cursor visible", () => checkVisible(cursorBoxes[0].Cursor));
+ AddAssert("Check global cursor alpha is 0", () => !checkVisible(globalCursorDisplay.MenuCursor) && globalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 0);
+
+ AddStep("Press key", () => InputManager.Key(Key.A));
+ AddAssert("Check green cursor still visible", () => checkVisible(cursorBoxes[0].Cursor));
+ AddAssert("Check global cursor alpha is still 0", () => !checkVisible(globalCursorDisplay.MenuCursor) && globalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 0);
+
+ if (clickToShow)
+ AddStep("Click mouse", () => InputManager.Click(MouseButton.Left));
+ else
+ AddStep("Move mouse", () => InputManager.MoveMouseTo(InputManager.CurrentState.Mouse.Position + Vector2.One));
+
+ AddAssert("Check green cursor still visible", () => checkVisible(cursorBoxes[0].Cursor));
+ AddAssert("Check global cursor alpha is still 0", () => !checkVisible(globalCursorDisplay.MenuCursor) && globalCursorDisplay.MenuCursor.ActiveCursor.Alpha == 0);
}
///
@@ -191,7 +243,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
public bool SmoothTransition;
- public CursorContainer MenuCursor { get; }
+ public CursorContainer Cursor { get; }
public bool ProvidingUserCursor { get; }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || (SmoothTransition && !ProvidingUserCursor);
@@ -218,7 +270,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Origin = Anchor.Centre,
Text = providesUserCursor ? "User cursor" : "Local cursor"
},
- MenuCursor = new TestCursorContainer
+ Cursor = new TestCursorContainer
{
State = { Value = providesUserCursor ? Visibility.Hidden : Visibility.Visible },
}
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.Tests/Visual/UserInterface/TestSceneSettingsToolboxGroup.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsToolboxGroup.cs
index 50e506f82b..9fb0905a4f 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsToolboxGroup.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsToolboxGroup.cs
@@ -5,12 +5,14 @@
using System.Linq;
using NUnit.Framework;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
+using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
@@ -20,11 +22,17 @@ namespace osu.Game.Tests.Visual.UserInterface
{
private SettingsToolboxGroup group;
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
+
[SetUp]
public void SetUp() => Schedule(() =>
{
Child = group = new SettingsToolboxGroup("example")
{
+ Scale = new Vector2(3),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
Children = new Drawable[]
{
new RoundedButton
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/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
index 56a432aec4..0a09e6e7e6 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs
@@ -9,11 +9,8 @@ using osuTK.Graphics;
namespace osu.Game.Beatmaps.ControlPoints
{
- public abstract class ControlPoint : IComparable, IDeepCloneable, IEquatable
+ public abstract class ControlPoint : IComparable, IDeepCloneable, IEquatable, IControlPoint
{
- ///
- /// The time at which the control point takes effect.
- ///
[JsonIgnore]
public double Time { get; set; }
diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
index 4be6b5eede..422e306450 100644
--- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
+++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs
@@ -196,8 +196,8 @@ namespace osu.Game.Beatmaps.ControlPoints
/// The time to find the control point at.
/// The control point to use when is before any control points.
/// The active control point at , or a fallback if none found.
- protected T BinarySearchWithFallback(IReadOnlyList list, double time, T fallback)
- where T : ControlPoint
+ public static T BinarySearchWithFallback(IReadOnlyList list, double time, T fallback)
+ where T : class, IControlPoint
{
return BinarySearch(list, time) ?? fallback;
}
@@ -207,9 +207,9 @@ namespace osu.Game.Beatmaps.ControlPoints
///
/// The list to search.
/// The time to find the control point at.
- /// The active control point at .
- protected virtual T BinarySearch(IReadOnlyList list, double time)
- where T : ControlPoint
+ /// The active control point at . Will return null if there are no control points, or if the time is before the first control point.
+ public static T BinarySearch(IReadOnlyList list, double time)
+ where T : class, IControlPoint
{
if (list == null)
throw new ArgumentNullException(nameof(list));
diff --git a/osu.Game/Beatmaps/ControlPoints/IControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/IControlPoint.cs
new file mode 100644
index 0000000000..091e99e029
--- /dev/null
+++ b/osu.Game/Beatmaps/ControlPoints/IControlPoint.cs
@@ -0,0 +1,13 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Beatmaps.ControlPoints
+{
+ public interface IControlPoint
+ {
+ ///
+ /// The time at which the control point takes effect.
+ ///
+ double Time { get; }
+ }
+}
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 75500fbc4e..5f0a2a0824 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs
@@ -355,6 +355,14 @@ namespace osu.Game.Beatmaps.Formats
switch (type)
{
+ case LegacyEventType.Sprite:
+ // Generally, the background is the first thing defined in a beatmap file.
+ // In some older beatmaps, it is not present and replaced by a storyboard-level background instead.
+ // Allow the first sprite (by file order) to act as the background in such cases.
+ if (string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.BackgroundFile))
+ beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[3]);
+ break;
+
case LegacyEventType.Background:
beatmap.BeatmapInfo.Metadata.BackgroundFile = CleanFilename(split[2]);
break;
@@ -427,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,
@@ -440,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/Cursor/GlobalCursorDisplay.cs b/osu.Game/Graphics/Cursor/GlobalCursorDisplay.cs
index 6613e18cbe..f5429723be 100644
--- a/osu.Game/Graphics/Cursor/GlobalCursorDisplay.cs
+++ b/osu.Game/Graphics/Cursor/GlobalCursorDisplay.cs
@@ -13,7 +13,7 @@ using osu.Game.Configuration;
namespace osu.Game.Graphics.Cursor
{
///
- /// A container which provides the main .
+ /// A container which provides the main .
/// Also handles cases where a more localised cursor is provided by another component (via ).
///
public class GlobalCursorDisplay : Container, IProvideCursor
@@ -23,7 +23,9 @@ namespace osu.Game.Graphics.Cursor
///
internal bool ShowCursor = true;
- public CursorContainer MenuCursor { get; }
+ CursorContainer IProvideCursor.Cursor => MenuCursor;
+
+ public MenuCursorContainer MenuCursor { get; }
public bool ProvidingUserCursor => true;
@@ -42,8 +44,8 @@ namespace osu.Game.Graphics.Cursor
{
AddRangeInternal(new Drawable[]
{
- MenuCursor = new MenuCursor { State = { Value = Visibility.Hidden } },
- Content = new Container { RelativeSizeAxes = Axes.Both }
+ Content = new Container { RelativeSizeAxes = Axes.Both },
+ MenuCursor = new MenuCursorContainer { State = { Value = Visibility.Hidden } }
});
}
@@ -64,7 +66,7 @@ namespace osu.Game.Graphics.Cursor
if (!hasValidInput || !ShowCursor)
{
- currentOverrideProvider?.MenuCursor?.Hide();
+ currentOverrideProvider?.Cursor?.Hide();
currentOverrideProvider = null;
return;
}
@@ -83,8 +85,8 @@ namespace osu.Game.Graphics.Cursor
if (currentOverrideProvider == newOverrideProvider)
return;
- currentOverrideProvider?.MenuCursor?.Hide();
- newOverrideProvider.MenuCursor?.Show();
+ currentOverrideProvider?.Cursor?.Hide();
+ newOverrideProvider.Cursor?.Show();
currentOverrideProvider = newOverrideProvider;
}
diff --git a/osu.Game/Graphics/Cursor/IProvideCursor.cs b/osu.Game/Graphics/Cursor/IProvideCursor.cs
index f7f7b75bc8..9f01e5da6d 100644
--- a/osu.Game/Graphics/Cursor/IProvideCursor.cs
+++ b/osu.Game/Graphics/Cursor/IProvideCursor.cs
@@ -17,10 +17,10 @@ namespace osu.Game.Graphics.Cursor
/// The cursor provided by this .
/// May be null if no cursor should be visible.
///
- CursorContainer MenuCursor { get; }
+ CursorContainer Cursor { get; }
///
- /// Whether should be displayed as the singular user cursor. This will temporarily hide any other user cursor.
+ /// Whether should be displayed as the singular user cursor. This will temporarily hide any other user cursor.
/// This value is checked every frame and may be used to control whether multiple cursors are displayed (e.g. watching replays).
///
bool ProvidingUserCursor { get; }
diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs
similarity index 69%
rename from osu.Game/Graphics/Cursor/MenuCursor.cs
rename to osu.Game/Graphics/Cursor/MenuCursorContainer.cs
index 862a10208c..af542989ff 100644
--- a/osu.Game/Graphics/Cursor/MenuCursor.cs
+++ b/osu.Game/Graphics/Cursor/MenuCursorContainer.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 osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@@ -21,24 +18,43 @@ using osuTK;
namespace osu.Game.Graphics.Cursor
{
- public class MenuCursor : CursorContainer
+ public class MenuCursorContainer : CursorContainer
{
private readonly IBindable screenshotCursorVisibility = new Bindable(true);
public override bool IsPresent => screenshotCursorVisibility.Value && base.IsPresent;
+ private bool hideCursorOnNonMouseInput;
+
+ public bool HideCursorOnNonMouseInput
+ {
+ get => hideCursorOnNonMouseInput;
+ set
+ {
+ if (hideCursorOnNonMouseInput == value)
+ return;
+
+ hideCursorOnNonMouseInput = value;
+ updateState();
+ }
+ }
+
protected override Drawable CreateCursor() => activeCursor = new Cursor();
- private Cursor activeCursor;
+ private Cursor activeCursor = null!;
- private Bindable cursorRotate;
private DragRotationState dragRotationState;
private Vector2 positionMouseDown;
-
- private Sample tapSample;
private Vector2 lastMovePosition;
- [BackgroundDependencyLoader(true)]
- private void load([NotNull] OsuConfigManager config, [CanBeNull] ScreenshotManager screenshotManager, AudioManager audio)
+ private Bindable cursorRotate = null!;
+ private Sample tapSample = null!;
+
+ private MouseInputDetector mouseInputDetector = null!;
+
+ private bool visible;
+
+ [BackgroundDependencyLoader]
+ private void load(OsuConfigManager config, ScreenshotManager? screenshotManager, AudioManager audio)
{
cursorRotate = config.GetBindable(OsuSetting.CursorRotation);
@@ -46,6 +62,45 @@ namespace osu.Game.Graphics.Cursor
screenshotCursorVisibility.BindTo(screenshotManager.CursorVisibility);
tapSample = audio.Samples.Get(@"UI/cursor-tap");
+
+ Add(mouseInputDetector = new MouseInputDetector());
+ }
+
+ [Resolved]
+ private OsuGame? game { get; set; }
+
+ private readonly IBindable lastInputWasMouse = new BindableBool();
+ private readonly IBindable isIdle = new BindableBool();
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ lastInputWasMouse.BindTo(mouseInputDetector.LastInputWasMouseSource);
+ lastInputWasMouse.BindValueChanged(_ => updateState(), true);
+
+ if (game != null)
+ {
+ isIdle.BindTo(game.IsIdle);
+ isIdle.BindValueChanged(_ => updateState());
+ }
+ }
+
+ protected override void UpdateState(ValueChangedEvent state) => updateState();
+
+ private void updateState()
+ {
+ bool combinedVisibility = State.Value == Visibility.Visible && (lastInputWasMouse.Value || !hideCursorOnNonMouseInput) && !isIdle.Value;
+
+ if (visible == combinedVisibility)
+ return;
+
+ visible = combinedVisibility;
+
+ if (visible)
+ PopIn();
+ else
+ PopOut();
}
protected override void Update()
@@ -163,11 +218,11 @@ namespace osu.Game.Graphics.Cursor
public class Cursor : Container
{
- private Container cursorContainer;
- private Bindable cursorScale;
+ private Container cursorContainer = null!;
+ private Bindable cursorScale = null!;
private const float base_scale = 0.15f;
- public Sprite AdditiveLayer;
+ public Sprite AdditiveLayer = null!;
public Cursor()
{
@@ -204,6 +259,40 @@ namespace osu.Game.Graphics.Cursor
}
}
+ private class MouseInputDetector : Component
+ {
+ ///
+ /// Whether the last input applied to the game is sourced from mouse.
+ ///
+ public IBindable LastInputWasMouseSource => lastInputWasMouseSource;
+
+ private readonly Bindable lastInputWasMouseSource = new Bindable();
+
+ public MouseInputDetector()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ protected override bool Handle(UIEvent e)
+ {
+ switch (e)
+ {
+ case MouseDownEvent:
+ case MouseMoveEvent:
+ lastInputWasMouseSource.Value = true;
+ return false;
+
+ case KeyDownEvent keyDown when !keyDown.Repeat:
+ case JoystickPressEvent:
+ case MidiDownEvent:
+ lastInputWasMouseSource.Value = false;
+ return false;
+ }
+
+ return false;
+ }
+ }
+
private enum DragRotationState
{
NotDragging,
diff --git a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs
index 7f86a060ad..e51dbeed14 100644
--- a/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs
+++ b/osu.Game/Graphics/UserInterface/ExternalLinkButton.cs
@@ -10,7 +10,6 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
-using osu.Game.Localisation;
using osu.Framework.Platform;
using osu.Game.Overlays;
using osu.Game.Overlays.OSD;
@@ -94,15 +93,7 @@ namespace osu.Game.Graphics.UserInterface
private void copyUrl()
{
host.GetClipboard()?.SetText(Link);
- onScreenDisplay?.Display(new CopyUrlToast(ToastStrings.UrlCopied));
- }
-
- private class CopyUrlToast : Toast
- {
- public CopyUrlToast(LocalisableString value)
- : base(UserInterfaceStrings.GeneralHeader, value, "")
- {
- }
+ onScreenDisplay?.Display(new CopyUrlToast());
}
}
}
diff --git a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs
index 6358317e9d..f0ff76b35d 100644
--- a/osu.Game/Graphics/UserInterface/HoverSampleSet.cs
+++ b/osu.Game/Graphics/UserInterface/HoverSampleSet.cs
@@ -15,6 +15,9 @@ namespace osu.Game.Graphics.UserInterface
[Description("button")]
Button,
+ [Description("button-sidebar")]
+ ButtonSidebar,
+
[Description("toolbar")]
Toolbar,
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/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
index 2a8b41fd20..9acb0c7f94 100644
--- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
+++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs
@@ -44,6 +44,8 @@ namespace osu.Game.Graphics.UserInterface
public virtual LocalisableString TooltipText { get; private set; }
+ public bool PlaySamplesOnAdjust { get; set; } = true;
+
///
/// Whether to format the tooltip as a percentage or the actual value.
///
@@ -187,6 +189,9 @@ namespace osu.Game.Graphics.UserInterface
private void playSample(T value)
{
+ if (!PlaySamplesOnAdjust)
+ return;
+
if (Clock == null || Clock.CurrentTime - lastSampleTime <= 30)
return;
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 2c9f250028..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")]
@@ -114,6 +115,7 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty("has_replay")]
public bool HasReplay { get; set; }
+ // These properties are calculated or not relevant to any external usage.
public bool ShouldSerializeID() => false;
public bool ShouldSerializeUser() => false;
public bool ShouldSerializeBeatmap() => false;
@@ -122,6 +124,18 @@ namespace osu.Game.Online.API.Requests.Responses
public bool ShouldSerializeOnlineID() => false;
public bool ShouldSerializeHasReplay() => false;
+ // These fields only need to be serialised if they hold values.
+ // Generally this is required because this model may be used by server-side components, but
+ // we don't want to bother sending these fields in score submission requests, for instance.
+ public bool ShouldSerializeEndedAt() => EndedAt != default;
+ public bool ShouldSerializeStartedAt() => StartedAt != default;
+ public bool ShouldSerializeLegacyScoreId() => LegacyScoreId != null;
+ public bool ShouldSerializeLegacyTotalScore() => LegacyTotalScore != null;
+ public bool ShouldSerializeMods() => Mods.Length > 0;
+ public bool ShouldSerializeUserID() => UserID > 0;
+ public bool ShouldSerializeBeatmapID() => BeatmapID > 0;
+ public bool ShouldSerializeBuildID() => BuildID != null;
+
#endregion
public override string ToString() => $"score_id: {ID} user_id: {UserID}";
@@ -140,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;
}
@@ -152,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/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs
index 6f597e5b10..d5e0c7a970 100644
--- a/osu.Game/Online/Rooms/MultiplayerScore.cs
+++ b/osu.Game/Online/Rooms/MultiplayerScore.cs
@@ -65,7 +65,7 @@ namespace osu.Game.Online.Rooms
[CanBeNull]
public MultiplayerScoresAround ScoresAround { get; set; }
- public ScoreInfo CreateScoreInfo(RulesetStore rulesets, PlaylistItem playlistItem, [NotNull] BeatmapInfo beatmap)
+ public ScoreInfo CreateScoreInfo(ScoreManager scoreManager, RulesetStore rulesets, PlaylistItem playlistItem, [NotNull] BeatmapInfo beatmap)
{
var ruleset = rulesets.GetRuleset(playlistItem.RulesetID);
if (ruleset == null)
@@ -90,6 +90,8 @@ namespace osu.Game.Online.Rooms
Position = Position,
};
+ scoreManager.PopulateMaximumStatistics(scoreInfo);
+
return scoreInfo;
}
}
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 1716e48395..4f8098136f 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -70,6 +70,7 @@ namespace osu.Game
/// The full osu! experience. Builds on top of to add menus and binding logic
/// for initial components that are generally retrieved via DI.
///
+ [Cached(typeof(OsuGame))]
public class OsuGame : OsuGameBase, IKeyBindingHandler, ILocalUserPlayInfo, IPerformFromScreenRunner, IOverlayManager, ILinkHandler
{
///
@@ -136,6 +137,11 @@ namespace osu.Game
private IdleTracker idleTracker;
+ ///
+ /// Whether the user is currently in an idle state.
+ ///
+ public IBindable IsIdle => idleTracker.IsIdle;
+
///
/// Whether overlays should be able to be opened game-wide. Value is sourced from the current active screen.
///
@@ -173,6 +179,8 @@ namespace osu.Game
private Bindable configRuleset;
+ private Bindable applySafeAreaConsiderations;
+
private Bindable uiScale;
private Bindable configSkin;
@@ -266,8 +274,6 @@ namespace osu.Game
[BackgroundDependencyLoader]
private void load()
{
- dependencies.CacheAs(this);
-
SentryLogger.AttachUser(API.LocalUser);
dependencies.Cache(osuLogo = new OsuLogo { Alpha = 0 });
@@ -276,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
{
@@ -308,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;
@@ -1329,6 +1335,8 @@ namespace osu.Game
OverlayActivationMode.BindTo(newOsuScreen.OverlayActivationMode);
API.Activity.BindTo(newOsuScreen.Activity);
+ GlobalCursorDisplay.MenuCursor.HideCursorOnNonMouseInput = newOsuScreen.HideMenuCursorOnNonMouseInput;
+
if (newOsuScreen.HideOverlaysOnEnter)
CloseAllOverlays();
else
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index 478f154d58..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;
@@ -62,6 +67,7 @@ namespace osu.Game
/// Unlike , this class will not load any kind of UI, allowing it to be used
/// for provide dependencies to test cases without interfering with them.
///
+ [Cached(typeof(OsuGameBase))]
public partial class OsuGameBase : Framework.Game, ICanAcceptFiles, IBeatSyncProvider
{
public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv" };
@@ -188,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.
@@ -253,7 +261,6 @@ namespace osu.Game
largeStore.AddTextureSource(Host.CreateTextureLoaderStore(new OnlineStore()));
dependencies.Cache(largeStore);
- dependencies.CacheAs(this);
dependencies.CacheAs(LocalConfig);
InitialiseFonts();
@@ -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/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs
index 193d15064a..0675d99a25 100644
--- a/osu.Game/Overlays/Comments/DrawableComment.cs
+++ b/osu.Game/Overlays/Comments/DrawableComment.cs
@@ -20,11 +20,13 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Extensions.IEnumerableExtensions;
using System.Collections.Specialized;
using osu.Framework.Localisation;
+using osu.Framework.Platform;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Comments.Buttons;
using osu.Game.Overlays.Dialog;
+using osu.Game.Overlays.OSD;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Comments
@@ -71,6 +73,12 @@ namespace osu.Game.Overlays.Comments
[Resolved]
private IAPIProvider api { get; set; } = null!;
+ [Resolved]
+ private GameHost host { get; set; } = null!;
+
+ [Resolved(canBeNull: true)]
+ private OnScreenDisplay? onScreenDisplay { get; set; }
+
public DrawableComment(Comment comment)
{
Comment = comment;
@@ -203,7 +211,6 @@ namespace osu.Game.Overlays.Comments
{
Name = @"Actions buttons",
AutoSizeAxes = Axes.Both,
- Spacing = new Vector2(10, 0)
},
actionsLoading = new LoadingSpinner
{
@@ -323,6 +330,9 @@ namespace osu.Game.Overlays.Comments
if (WasDeleted)
makeDeleted();
+ actionsContainer.AddLink("Copy link", copyUrl);
+ actionsContainer.AddArbitraryDrawable(new Container { Width = 10 });
+
if (Comment.UserId.HasValue && Comment.UserId.Value == api.LocalUser.Value.Id)
{
actionsContainer.AddLink("Delete", deleteComment);
@@ -403,6 +413,12 @@ namespace osu.Game.Overlays.Comments
api.Queue(request);
}
+ private void copyUrl()
+ {
+ host.GetClipboard()?.SetText($@"{api.APIEndpointUrl}/comments/{Comment.Id}");
+ onScreenDisplay?.Display(new CopyUrlToast());
+ }
+
protected override void LoadComplete()
{
ShowDeleted.BindValueChanged(show =>
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/OSD/CopyUrlToast.cs b/osu.Game/Overlays/OSD/CopyUrlToast.cs
new file mode 100644
index 0000000000..ea835a1c5e
--- /dev/null
+++ b/osu.Game/Overlays/OSD/CopyUrlToast.cs
@@ -0,0 +1,15 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Localisation;
+
+namespace osu.Game.Overlays.OSD
+{
+ public class CopyUrlToast : Toast
+ {
+ public CopyUrlToast()
+ : base(UserInterfaceStrings.GeneralHeader, ToastStrings.UrlCopied, "")
+ {
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs
index cea8fdd733..8f188f04d9 100644
--- a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs
@@ -8,6 +8,7 @@ using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Configuration;
+using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
namespace osu.Game.Overlays.Settings.Sections.Audio
@@ -21,7 +22,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
{
Children = new Drawable[]
{
- new SettingsSlider
+ new VolumeAdjustSlider
{
LabelText = AudioSettingsStrings.MasterVolume,
Current = audio.Volume,
@@ -35,14 +36,15 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
KeyboardStep = 0.01f,
DisplayAsPercentage = true
},
- new SettingsSlider
+ new VolumeAdjustSlider
{
LabelText = AudioSettingsStrings.EffectVolume,
Current = audio.VolumeSample,
KeyboardStep = 0.01f,
DisplayAsPercentage = true
},
- new SettingsSlider
+
+ new VolumeAdjustSlider
{
LabelText = AudioSettingsStrings.MusicVolume,
Current = audio.VolumeTrack,
@@ -51,5 +53,15 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
},
};
}
+
+ private class VolumeAdjustSlider : SettingsSlider
+ {
+ protected override Drawable CreateControl()
+ {
+ var sliderBar = (OsuSliderBar)base.CreateControl();
+ sliderBar.PlaySamplesOnAdjust = false;
+ return sliderBar;
+ }
+ }
}
}
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/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs
index 2fea2e34b2..c91a6a48d4 100644
--- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs
+++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs
@@ -327,6 +327,50 @@ namespace osu.Game.Overlays.Settings.Sections.Input
finalise();
}
+ protected override bool OnTabletAuxiliaryButtonPress(TabletAuxiliaryButtonPressEvent e)
+ {
+ if (!HasFocus)
+ return false;
+
+ bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState));
+ finalise();
+
+ return true;
+ }
+
+ protected override void OnTabletAuxiliaryButtonRelease(TabletAuxiliaryButtonReleaseEvent e)
+ {
+ if (!HasFocus)
+ {
+ base.OnTabletAuxiliaryButtonRelease(e);
+ return;
+ }
+
+ finalise();
+ }
+
+ protected override bool OnTabletPenButtonPress(TabletPenButtonPressEvent e)
+ {
+ if (!HasFocus)
+ return false;
+
+ bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(e.CurrentState));
+ finalise();
+
+ return true;
+ }
+
+ protected override void OnTabletPenButtonRelease(TabletPenButtonReleaseEvent e)
+ {
+ if (!HasFocus)
+ {
+ base.OnTabletPenButtonRelease(e);
+ return;
+ }
+
+ finalise();
+ }
+
private void clear()
{
if (bindTarget == null)
diff --git a/osu.Game/Overlays/Settings/SidebarButton.cs b/osu.Game/Overlays/Settings/SidebarButton.cs
index c6a4cbbcaa..2c4832c68a 100644
--- a/osu.Game/Overlays/Settings/SidebarButton.cs
+++ b/osu.Game/Overlays/Settings/SidebarButton.cs
@@ -16,6 +16,11 @@ namespace osu.Game.Overlays.Settings
[Resolved]
protected OverlayColourProvider ColourProvider { get; private set; }
+ protected SidebarButton()
+ : base(HoverSampleSet.ButtonSidebar)
+ {
+ }
+
[BackgroundDependencyLoader]
private void load()
{
diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs
index 548d75c9a9..6dd9e2a56d 100644
--- a/osu.Game/Overlays/SettingsToolboxGroup.cs
+++ b/osu.Game/Overlays/SettingsToolboxGroup.cs
@@ -1,8 +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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Extensions.EnumExtensions;
@@ -23,25 +22,38 @@ namespace osu.Game.Overlays
{
public class SettingsToolboxGroup : Container, IExpandable
{
+ private readonly string title;
public const int CONTAINER_WIDTH = 270;
private const float transition_duration = 250;
- private const int border_thickness = 2;
private const int header_height = 30;
private const int corner_radius = 5;
- private const float fade_duration = 800;
- private const float inactive_alpha = 0.5f;
-
private readonly Cached headerTextVisibilityCache = new Cached();
- private readonly FillFlowContainer content;
+ protected override Container Content => content;
+
+ private readonly FillFlowContainer content = new FillFlowContainer
+ {
+ Name = @"Content",
+ Origin = Anchor.TopCentre,
+ Anchor = Anchor.TopCentre,
+ Direction = FillDirection.Vertical,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Padding = new MarginPadding { Horizontal = 10, Top = 5, Bottom = 10 },
+ Spacing = new Vector2(0, 15),
+ };
public BindableBool Expanded { get; } = new BindableBool(true);
- private readonly OsuSpriteText headerText;
+ private OsuSpriteText headerText = null!;
- private readonly Container headerContent;
+ private Container headerContent = null!;
+
+ private Box background = null!;
+
+ private IconButton expandButton = null!;
///
/// Create a new instance.
@@ -49,20 +61,25 @@ namespace osu.Game.Overlays
/// The title to be displayed in the header of this group.
public SettingsToolboxGroup(string title)
{
+ this.title = title;
+
AutoSizeAxes = Axes.Y;
Width = CONTAINER_WIDTH;
Masking = true;
+ }
+
+ [BackgroundDependencyLoader(true)]
+ private void load(OverlayColourProvider? colourProvider)
+ {
CornerRadius = corner_radius;
- BorderColour = Color4.Black;
- BorderThickness = border_thickness;
InternalChildren = new Drawable[]
{
- new Box
+ background = new Box
{
RelativeSizeAxes = Axes.Both,
- Colour = Color4.Black,
- Alpha = 0.5f,
+ Alpha = 0.1f,
+ Colour = colourProvider?.Background4 ?? Color4.Black,
},
new FillFlowContainer
{
@@ -88,7 +105,7 @@ namespace osu.Game.Overlays
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 17),
Padding = new MarginPadding { Left = 10, Right = 30 },
},
- new IconButton
+ expandButton = new IconButton
{
Origin = Anchor.Centre,
Anchor = Anchor.CentreRight,
@@ -99,19 +116,7 @@ namespace osu.Game.Overlays
},
}
},
- content = new FillFlowContainer
- {
- Name = @"Content",
- Origin = Anchor.TopCentre,
- Anchor = Anchor.TopCentre,
- Direction = FillDirection.Vertical,
- RelativeSizeAxes = Axes.X,
- AutoSizeDuration = transition_duration,
- AutoSizeEasing = Easing.OutQuint,
- AutoSizeAxes = Axes.Y,
- Padding = new MarginPadding(15),
- Spacing = new Vector2(0, 15),
- }
+ content
}
},
};
@@ -175,9 +180,10 @@ namespace osu.Game.Overlays
private void updateFadeState()
{
- this.FadeTo(IsHovered ? 1 : inactive_alpha, fade_duration, Easing.OutQuint);
- }
+ const float fade_duration = 500;
- protected override Container Content => content;
+ background.FadeTo(IsHovered ? 1 : 0.1f, fade_duration, Easing.OutQuint);
+ expandButton.FadeTo(IsHovered ? 1 : 0, fade_duration, Easing.OutQuint);
+ }
}
}
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 4726211666..b0a2694a0a 100644
--- a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs
@@ -3,14 +3,21 @@
#nullable disable
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
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;
@@ -18,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
{
@@ -30,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,
@@ -42,35 +50,114 @@ 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)
{
}
[BackgroundDependencyLoader]
- private void load()
+ private void load(OverlayColourProvider colourProvider)
{
- AddInternal(RightSideToolboxContainer = new ExpandingToolboxContainer(130, 250)
+ AddInternal(new Container
{
- Padding = new MarginPadding(10),
- Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
- Child = new EditorToolboxGroup("snapping")
+ RelativeSizeAxes = Axes.Y,
+ AutoSizeAxes = Axes.X,
+ Children = new Drawable[]
{
- Child = distanceSpacingSlider = new ExpandableSlider>
+ new Box
{
- Current = { BindTarget = DistanceSpacingMultiplier },
- KeyboardStep = adjust_step,
+ Colour = colourProvider.Background5,
+ RelativeSizeAxes = Axes.Both,
+ },
+ RightSideToolboxContainer = new ExpandingToolboxContainer(130, 250)
+ {
+ Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1,
+ Child = new EditorToolboxGroup("snapping")
+ {
+ Children = new Drawable[]
+ {
+ 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,
+ }
+ }
+ }
}
}
});
}
+ 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();
@@ -88,22 +175,61 @@ 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);
}
}
- public bool OnPressed(KeyBindingPressEvent e)
+ 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;
+ }
+ }
+
+ public virtual bool OnPressed(KeyBindingPressEvent e)
{
switch (e.Action)
{
case GlobalAction.EditorIncreaseDistanceSpacing:
case GlobalAction.EditorDecreaseDistanceSpacing:
- return adjustDistanceSpacing(e.Action, adjust_step);
+ return AdjustDistanceSpacing(e.Action, adjust_step);
}
return false;
}
- public void OnReleased(KeyBindingReleaseEvent e)
+ public virtual void OnReleased(KeyBindingReleaseEvent e)
{
}
@@ -113,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;
@@ -129,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/ExpandingToolboxContainer.cs b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs
index d3371d3543..26dd5dfa55 100644
--- a/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs
+++ b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs
@@ -19,7 +19,8 @@ namespace osu.Game.Rulesets.Edit
{
RelativeSizeAxes = Axes.Y;
- FillFlow.Spacing = new Vector2(10);
+ FillFlow.Spacing = new Vector2(5);
+ Padding = new MarginPadding { Vertical = 5 };
}
protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && anyToolboxHovered(screenSpacePos);
diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
index 37c66037eb..520fcb0290 100644
--- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
@@ -12,10 +12,12 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
+using osu.Game.Overlays;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
@@ -80,7 +82,7 @@ namespace osu.Game.Rulesets.Edit
dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
[BackgroundDependencyLoader]
- private void load()
+ private void load(OverlayColourProvider colourProvider)
{
Config = Dependencies.Get().GetConfigFor(Ruleset);
@@ -102,7 +104,7 @@ namespace osu.Game.Rulesets.Edit
InternalChildren = new Drawable[]
{
- new Container
+ PlayfieldContentContainer = new Container
{
Name = "Content",
RelativeSizeAxes = Axes.Both,
@@ -116,25 +118,37 @@ namespace osu.Game.Rulesets.Edit
.WithChild(BlueprintContainer = CreateBlueprintContainer())
}
},
- new ExpandingToolboxContainer(90, 200)
+ new Container
{
- Padding = new MarginPadding(10),
+ RelativeSizeAxes = Axes.Y,
+ AutoSizeAxes = Axes.X,
Children = new Drawable[]
{
- new EditorToolboxGroup("toolbox (1-9)")
+ new Box
{
- Child = toolboxCollection = new EditorRadioButtonCollection { RelativeSizeAxes = Axes.X }
+ Colour = colourProvider.Background5,
+ RelativeSizeAxes = Axes.Both,
},
- new EditorToolboxGroup("toggles (Q~P)")
+ new ExpandingToolboxContainer(60, 200)
{
- Child = togglesCollection = new FillFlowContainer
+ Children = new Drawable[]
{
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Direction = FillDirection.Vertical,
- Spacing = new Vector2(0, 5),
- },
- }
+ new EditorToolboxGroup("toolbox (1-9)")
+ {
+ Child = toolboxCollection = new EditorRadioButtonCollection { RelativeSizeAxes = Axes.X }
+ },
+ new EditorToolboxGroup("toggles (Q~P)")
+ {
+ Child = togglesCollection = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 5),
+ },
+ }
+ }
+ },
}
},
};
@@ -152,6 +166,15 @@ namespace osu.Game.Rulesets.Edit
EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged;
}
+ ///
+ /// Houses all content relevant to the playfield.
+ ///
+ ///
+ /// Generally implementations should not be adding to this directly.
+ /// Use or instead.
+ ///
+ protected Container PlayfieldContentContainer { get; private set; }
+
protected override void LoadComplete()
{
base.LoadComplete();
@@ -215,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/Rulesets/Timing/MultiplierControlPoint.cs b/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs
index 1e80bd165b..279de2f940 100644
--- a/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs
+++ b/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs
@@ -11,12 +11,12 @@ namespace osu.Game.Rulesets.Timing
///
/// A control point which adds an aggregated multiplier based on the provided 's BeatLength and 's SpeedMultiplier.
///
- public class MultiplierControlPoint : IComparable
+ public class MultiplierControlPoint : IComparable, IControlPoint
{
///
/// The time in milliseconds at which this starts.
///
- public double StartTime;
+ public double Time { get; set; }
///
/// The aggregate multiplier which this provides.
@@ -54,13 +54,13 @@ namespace osu.Game.Rulesets.Timing
///
/// Creates a .
///
- /// The start time of this .
- public MultiplierControlPoint(double startTime)
+ /// The start time of this .
+ public MultiplierControlPoint(double time)
{
- StartTime = startTime;
+ Time = time;
}
// ReSharper disable once ImpureMethodCallOnReadonlyValueField
- public int CompareTo(MultiplierControlPoint other) => StartTime.CompareTo(other?.StartTime);
+ public int CompareTo(MultiplierControlPoint other) => Time.CompareTo(other?.Time);
}
}
diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs
index 73acb1759f..dd3a950264 100644
--- a/osu.Game/Rulesets/UI/DrawableRuleset.cs
+++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs
@@ -384,7 +384,7 @@ namespace osu.Game.Rulesets.UI
// only show the cursor when within the playfield, by default.
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Playfield.ReceivePositionalInputAt(screenSpacePos);
- CursorContainer IProvideCursor.MenuCursor => Playfield.Cursor;
+ CursorContainer IProvideCursor.Cursor => Playfield.Cursor;
public override GameplayCursorContainer Cursor => Playfield.Cursor;
@@ -499,6 +499,7 @@ namespace osu.Game.Rulesets.UI
///
/// The cursor being displayed by the . May be null if no cursor is provided.
///
+ [CanBeNull]
public abstract GameplayCursorContainer Cursor { get; }
///
diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs
index 2ec72d8fe3..e59e45722a 100644
--- a/osu.Game/Rulesets/UI/Playfield.cs
+++ b/osu.Game/Rulesets/UI/Playfield.cs
@@ -202,16 +202,14 @@ namespace osu.Game.Rulesets.UI
///
/// The cursor currently being used by this . May be null if no cursor is provided.
///
+ [CanBeNull]
public GameplayCursorContainer Cursor { get; private set; }
///
/// Provide a cursor which is to be used for gameplay.
///
- ///
- /// The default provided cursor is invisible when inside the bounds of the .
- ///
/// The cursor, or null to show the menu cursor.
- protected virtual GameplayCursorContainer CreateCursor() => new InvisibleCursorContainer();
+ protected virtual GameplayCursorContainer CreateCursor() => null;
///
/// Registers a as a nested .
@@ -522,14 +520,5 @@ namespace osu.Game.Rulesets.UI
}
#endregion
-
- public class InvisibleCursorContainer : GameplayCursorContainer
- {
- protected override Drawable CreateCursor() => new InvisibleCursor();
-
- private class InvisibleCursor : Drawable
- {
- }
- }
}
}
diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs
index 0bd8aa64c9..c957a84eb1 100644
--- a/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/ConstantScrollAlgorithm.cs
@@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
return -PositionAt(startTime, endTime, timeRange, scrollLength);
}
- public float PositionAt(double time, double currentTime, double timeRange, float scrollLength)
+ public float PositionAt(double time, double currentTime, double timeRange, float scrollLength, double? originTime = null)
=> (float)((time - currentTime) / timeRange * scrollLength);
public double TimeAt(float position, double currentTime, double timeRange, float scrollLength)
diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/IScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/IScrollAlgorithm.cs
index d2fb9e3531..f78509f919 100644
--- a/osu.Game/Rulesets/UI/Scrolling/Algorithms/IScrollAlgorithm.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/IScrollAlgorithm.cs
@@ -53,8 +53,9 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
/// The current time.
/// The amount of visible time.
/// The absolute spatial length through .
+ /// The time to be used for control point lookups (ie. the parent's start time for nested hit objects).
/// The absolute spatial position.
- float PositionAt(double time, double currentTime, double timeRange, float scrollLength);
+ float PositionAt(double time, double currentTime, double timeRange, float scrollLength, double? originTime = null);
///
/// Computes the time which brings a point to a provided spatial position given the current time.
@@ -63,7 +64,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
/// The current time.
/// The amount of visible time.
/// The absolute spatial length through .
- /// The time at which == .
+ /// The time at which == .
double TimeAt(float position, double currentTime, double timeRange, float scrollLength);
///
diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/OverlappingScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/OverlappingScrollAlgorithm.cs
index d41117bce8..54079c7895 100644
--- a/osu.Game/Rulesets/UI/Scrolling/Algorithms/OverlappingScrollAlgorithm.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/OverlappingScrollAlgorithm.cs
@@ -4,22 +4,20 @@
#nullable disable
using System;
+using System.Linq;
using osu.Framework.Lists;
+using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Timing;
namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
{
public class OverlappingScrollAlgorithm : IScrollAlgorithm
{
- private readonly MultiplierControlPoint searchPoint;
-
private readonly SortedList controlPoints;
public OverlappingScrollAlgorithm(SortedList controlPoints)
{
this.controlPoints = controlPoints;
-
- searchPoint = new MultiplierControlPoint();
}
public double GetDisplayStartTime(double originTime, float offset, double timeRange, float scrollLength)
@@ -37,8 +35,8 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
return -PositionAt(startTime, endTime, timeRange, scrollLength);
}
- public float PositionAt(double time, double currentTime, double timeRange, float scrollLength)
- => (float)((time - currentTime) / timeRange * controlPointAt(time).Multiplier * scrollLength);
+ public float PositionAt(double time, double currentTime, double timeRange, float scrollLength, double? originTime = null)
+ => (float)((time - currentTime) / timeRange * controlPointAt(originTime ?? time).Multiplier * scrollLength);
public double TimeAt(float position, double currentTime, double timeRange, float scrollLength)
{
@@ -52,7 +50,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
for (; i < controlPoints.Count; i++)
{
float lastPos = pos;
- pos = PositionAt(controlPoints[i].StartTime, currentTime, timeRange, scrollLength);
+ pos = PositionAt(controlPoints[i].Time, currentTime, timeRange, scrollLength);
if (pos > position)
{
@@ -64,7 +62,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
i = Math.Clamp(i, 0, controlPoints.Count - 1);
- return controlPoints[i].StartTime + (position - pos) * timeRange / controlPoints[i].Multiplier / scrollLength;
+ return controlPoints[i].Time + (position - pos) * timeRange / controlPoints[i].Multiplier / scrollLength;
}
public void Reset()
@@ -78,19 +76,11 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
/// The .
private MultiplierControlPoint controlPointAt(double time)
{
- if (controlPoints.Count == 0)
- return new MultiplierControlPoint(double.NegativeInfinity);
-
- if (time < controlPoints[0].StartTime)
- return controlPoints[0];
-
- searchPoint.StartTime = time;
- int index = controlPoints.BinarySearch(searchPoint);
-
- if (index < 0)
- index = ~index - 1;
-
- return controlPoints[index];
+ return ControlPointInfo.BinarySearch(controlPoints, time)
+ // The standard binary search will fail if there's no control points, or if the time is before the first.
+ // For this method, we want to use the first control point in the latter case.
+ ?? controlPoints.FirstOrDefault()
+ ?? new MultiplierControlPoint(double.NegativeInfinity);
}
}
}
diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs
index bfddc22573..774beb20c7 100644
--- a/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
return (float)(objectLength * scrollLength);
}
- public float PositionAt(double time, double currentTime, double timeRange, float scrollLength)
+ public float PositionAt(double time, double currentTime, double timeRange, float scrollLength, double? originTime = null)
{
double timelineLength = relativePositionAt(time, timeRange) - relativePositionAt(currentTime, timeRange);
return (float)(timelineLength * scrollLength);
@@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
if (controlPoints.Count == 0)
return;
- positionMappings.Add(new PositionMapping(controlPoints[0].StartTime, controlPoints[0]));
+ positionMappings.Add(new PositionMapping(controlPoints[0].Time, controlPoints[0]));
for (int i = 0; i < controlPoints.Count - 1; i++)
{
@@ -129,9 +129,9 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
var next = controlPoints[i + 1];
// Figure out how much of the time range the duration represents, and adjust it by the speed multiplier
- float length = (float)((next.StartTime - current.StartTime) / timeRange * current.Multiplier);
+ float length = (float)((next.Time - current.Time) / timeRange * current.Multiplier);
- positionMappings.Add(new PositionMapping(next.StartTime, next, positionMappings[^1].Position + length));
+ positionMappings.Add(new PositionMapping(next.Time, next, positionMappings[^1].Position + length));
}
}
diff --git a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
index 825aba5bc2..68469d083c 100644
--- a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
@@ -158,9 +158,9 @@ namespace osu.Game.Rulesets.UI.Scrolling
// Trim unwanted sequences of timing changes
timingChanges = timingChanges
// Collapse sections after the last hit object
- .Where(s => s.StartTime <= lastObjectTime)
+ .Where(s => s.Time <= lastObjectTime)
// Collapse sections with the same start time
- .GroupBy(s => s.StartTime).Select(g => g.Last()).OrderBy(s => s.StartTime);
+ .GroupBy(s => s.Time).Select(g => g.Last()).OrderBy(s => s.Time);
ControlPoints.AddRange(timingChanges);
diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
index 37da157cc1..424fc7c44c 100644
--- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
@@ -93,9 +93,9 @@ namespace osu.Game.Rulesets.UI.Scrolling
///
/// Given a time, return the position along the scrolling axis within this at time .
///
- public float PositionAtTime(double time, double currentTime)
+ public float PositionAtTime(double time, double currentTime, double? originTime = null)
{
- float scrollPosition = scrollingInfo.Algorithm.PositionAt(time, currentTime, timeRange.Value, scrollLength);
+ float scrollPosition = scrollingInfo.Algorithm.PositionAt(time, currentTime, timeRange.Value, scrollLength, originTime);
return axisInverted ? -scrollPosition : scrollPosition;
}
@@ -236,8 +236,10 @@ namespace osu.Game.Rulesets.UI.Scrolling
entry.LifetimeStart = Math.Min(entry.HitObject.StartTime - judgementOffset, computedStartTime);
}
- private void updateLayoutRecursive(DrawableHitObject hitObject)
+ private void updateLayoutRecursive(DrawableHitObject hitObject, double? parentHitObjectStartTime = null)
{
+ parentHitObjectStartTime ??= hitObject.HitObject.StartTime;
+
if (hitObject.HitObject is IHasDuration e)
{
float length = LengthAtTime(hitObject.HitObject.StartTime, e.EndTime);
@@ -249,17 +251,17 @@ namespace osu.Game.Rulesets.UI.Scrolling
foreach (var obj in hitObject.NestedHitObjects)
{
- updateLayoutRecursive(obj);
+ updateLayoutRecursive(obj, parentHitObjectStartTime);
// Nested hitobjects don't need to scroll, but they do need accurate positions and start lifetime
- updatePosition(obj, hitObject.HitObject.StartTime);
+ updatePosition(obj, hitObject.HitObject.StartTime, parentHitObjectStartTime);
setComputedLifetimeStart(obj.Entry);
}
}
- private void updatePosition(DrawableHitObject hitObject, double currentTime)
+ private void updatePosition(DrawableHitObject hitObject, double currentTime, double? parentHitObjectStartTime = null)
{
- float position = PositionAtTime(hitObject.HitObject.StartTime, currentTime);
+ float position = PositionAtTime(hitObject.HitObject.StartTime, currentTime, parentHitObjectStartTime);
if (scrollingAxis == Direction.Horizontal)
hitObject.X = position;
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/Components/RadioButtons/EditorRadioButton.cs b/osu.Game/Screens/Edit/Components/RadioButtons/EditorRadioButton.cs
index 82e94dc862..071bb9fdcb 100644
--- a/osu.Game/Screens/Edit/Components/RadioButtons/EditorRadioButton.cs
+++ b/osu.Game/Screens/Edit/Components/RadioButtons/EditorRadioButton.cs
@@ -8,13 +8,12 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
-using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
-using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
@@ -30,9 +29,9 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
public readonly RadioButton Button;
private Color4 defaultBackgroundColour;
- private Color4 defaultBubbleColour;
+ private Color4 defaultIconColour;
private Color4 selectedBackgroundColour;
- private Color4 selectedBubbleColour;
+ private Color4 selectedIconColour;
private Drawable icon;
@@ -50,20 +49,13 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load(OverlayColourProvider colourProvider)
{
- defaultBackgroundColour = colours.Gray3;
- defaultBubbleColour = defaultBackgroundColour.Darken(0.5f);
- selectedBackgroundColour = colours.BlueDark;
- selectedBubbleColour = selectedBackgroundColour.Lighten(0.5f);
+ defaultBackgroundColour = colourProvider.Background3;
+ selectedBackgroundColour = colourProvider.Background1;
- Content.EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Shadow,
- Radius = 2,
- Offset = new Vector2(0, 1),
- Colour = Color4.Black.Opacity(0.5f)
- };
+ defaultIconColour = defaultBackgroundColour.Darken(0.5f);
+ selectedIconColour = selectedBackgroundColour.Lighten(0.5f);
Add(icon = (Button.CreateIcon?.Invoke() ?? new Circle()).With(b =>
{
@@ -98,7 +90,7 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
return;
BackgroundColour = Button.Selected.Value ? selectedBackgroundColour : defaultBackgroundColour;
- icon.Colour = Button.Selected.Value ? selectedBubbleColour : defaultBubbleColour;
+ icon.Colour = Button.Selected.Value ? selectedIconColour : defaultIconColour;
}
protected override SpriteText CreateText() => new OsuSpriteText
diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs
index 55302833c1..1fb5c0285d 100644
--- a/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs
+++ b/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs
@@ -6,12 +6,11 @@
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
-using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
-using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
@@ -20,9 +19,9 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
internal class DrawableTernaryButton : OsuButton
{
private Color4 defaultBackgroundColour;
- private Color4 defaultBubbleColour;
+ private Color4 defaultIconColour;
private Color4 selectedBackgroundColour;
- private Color4 selectedBubbleColour;
+ private Color4 selectedIconColour;
private Drawable icon;
@@ -38,20 +37,13 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load(OverlayColourProvider colourProvider)
{
- defaultBackgroundColour = colours.Gray3;
- defaultBubbleColour = defaultBackgroundColour.Darken(0.5f);
- selectedBackgroundColour = colours.BlueDark;
- selectedBubbleColour = selectedBackgroundColour.Lighten(0.5f);
+ defaultBackgroundColour = colourProvider.Background3;
+ selectedBackgroundColour = colourProvider.Background1;
- Content.EdgeEffect = new EdgeEffectParameters
- {
- Type = EdgeEffectType.Shadow,
- Radius = 2,
- Offset = new Vector2(0, 1),
- Colour = Color4.Black.Opacity(0.5f)
- };
+ defaultIconColour = defaultBackgroundColour.Darken(0.5f);
+ selectedIconColour = selectedBackgroundColour.Lighten(0.5f);
Add(icon = (Button.CreateIcon?.Invoke() ?? new Circle()).With(b =>
{
@@ -85,17 +77,17 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
switch (Button.Bindable.Value)
{
case TernaryState.Indeterminate:
- icon.Colour = selectedBubbleColour.Darken(0.5f);
+ icon.Colour = selectedIconColour.Darken(0.5f);
BackgroundColour = selectedBackgroundColour.Darken(0.5f);
break;
case TernaryState.False:
- icon.Colour = defaultBubbleColour;
+ icon.Colour = defaultIconColour;
BackgroundColour = defaultBackgroundColour;
break;
case TernaryState.True:
- icon.Colour = selectedBubbleColour;
+ icon.Colour = selectedIconColour;
BackgroundColour = selectedBackgroundColour;
break;
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs
index 40403e08ad..6dca799549 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs
@@ -123,16 +123,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
},
new Drawable[]
- {
- new TextFlowContainer(s => s.Font = s.Font.With(size: 14))
- {
- Padding = new MarginPadding { Horizontal = 15 },
- Text = "beat snap",
- RelativeSizeAxes = Axes.X,
- TextAnchor = Anchor.TopCentre
- },
- },
- new Drawable[]
{
new Container
{
@@ -173,6 +163,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
}
},
+ new Drawable[]
+ {
+ new TextFlowContainer(s => s.Font = s.Font.With(size: 14))
+ {
+ Padding = new MarginPadding { Horizontal = 15, Vertical = 8 },
+ Text = "beat snap",
+ RelativeSizeAxes = Axes.X,
+ TextAnchor = Anchor.TopCentre,
+ },
+ },
},
RowDimensions = new[]
{
@@ -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/Compose/Components/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs
index c2415ce978..58d378154a 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs
@@ -78,16 +78,16 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
LabelText = "Waveform",
Current = { Value = true },
},
- controlPointsCheckbox = new OsuCheckbox
- {
- LabelText = "Control Points",
- Current = { Value = true },
- },
ticksCheckbox = new OsuCheckbox
{
LabelText = "Ticks",
Current = { Value = true },
- }
+ },
+ controlPointsCheckbox = new OsuCheckbox
+ {
+ LabelText = "BPM",
+ Current = { Value = true },
+ },
}
}
}
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/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs
index 7d8657a3df..a5739a41b1 100644
--- a/osu.Game/Screens/IOsuScreen.cs
+++ b/osu.Game/Screens/IOsuScreen.cs
@@ -41,6 +41,11 @@ namespace osu.Game.Screens
///
bool HideOverlaysOnEnter { get; }
+ ///
+ /// Whether the menu cursor should be hidden when non-mouse input is received.
+ ///
+ bool HideMenuCursorOnNonMouseInput { get; }
+
///
/// Whether overlays should be able to be opened when this screen is current.
///
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/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs
index 39740e650f..ba6b482729 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/Playlist/MultiplayerQueueList.cs
@@ -78,9 +78,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
return;
bool isItemOwner = Item.OwnerID == api.LocalUser.Value.OnlineID || multiplayerClient.IsHost;
+ bool isValidItem = isItemOwner && !Item.Expired;
- AllowDeletion = isItemOwner && !Item.Expired && Item.ID != multiplayerClient.Room.Settings.PlaylistItemId;
- AllowEditing = isItemOwner && !Item.Expired;
+ AllowDeletion = isValidItem
+ && (Item.ID != multiplayerClient.Room.Settings.PlaylistItemId // This is an optimisation for the following check.
+ || multiplayerClient.Room.Playlist.Count(i => !i.Expired) > 1);
+
+ AllowEditing = isValidItem;
}
protected override void Dispose(bool isDisposing)
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs
index 41633c34ce..27193d3cb6 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs
@@ -182,7 +182,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
/// An optional pivot around which the scores were retrieved.
private void performSuccessCallback([NotNull] Action> callback, [NotNull] List scores, [CanBeNull] MultiplayerScores pivot = null) => Schedule(() =>
{
- var scoreInfos = scoreManager.OrderByTotalScore(scores.Select(s => s.CreateScoreInfo(rulesets, playlistItem, Beatmap.Value.BeatmapInfo))).ToArray();
+ var scoreInfos = scoreManager.OrderByTotalScore(scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, playlistItem, Beatmap.Value.BeatmapInfo))).ToArray();
// Select a score if we don't already have one selected.
// Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll).
diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs
index 0678a90f71..6be13bbda3 100644
--- a/osu.Game/Screens/OsuScreen.cs
+++ b/osu.Game/Screens/OsuScreen.cs
@@ -40,11 +40,10 @@ namespace osu.Game.Screens
public virtual bool AllowExternalScreenChange => false;
- ///
- /// Whether all overlays should be hidden when this screen is entered or resumed.
- ///
public virtual bool HideOverlaysOnEnter => false;
+ public virtual bool HideMenuCursorOnNonMouseInput => false;
+
///
/// The initial overlay activation mode to use when this screen is entered for the first time.
///
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 747f4d4a8a..e7b2ce1672 100644
--- a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs
+++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs
@@ -15,6 +15,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
@@ -44,8 +45,8 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
public Bindable LabelStyle { get; } = new Bindable(LabelStyles.Icons);
private SpriteIcon arrow;
- private Drawable labelEarly;
- private Drawable labelLate;
+ private UprightAspectMaintainingContainer labelEarly;
+ private UprightAspectMaintainingContainer labelLate;
private Container colourBarsEarly;
private Container colourBarsLate;
@@ -122,6 +123,20 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
RelativeSizeAxes = Axes.Y,
Width = judgement_line_width,
},
+ labelEarly = new UprightAspectMaintainingContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.Centre,
+ Y = -10,
+ },
+ labelLate = new UprightAspectMaintainingContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.Centre,
+ Y = 10,
+ },
}
},
arrowContainer = new Container
@@ -261,57 +276,41 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
{
const float icon_size = 14;
- labelEarly?.Expire();
- labelEarly = null;
-
- labelLate?.Expire();
- labelLate = null;
-
switch (style)
{
case LabelStyles.None:
+ labelEarly.Clear();
+ labelLate.Clear();
break;
case LabelStyles.Icons:
- labelEarly = new SpriteIcon
+ labelEarly.Child = new SpriteIcon
{
- Y = -10,
Size = new Vector2(icon_size),
Icon = FontAwesome.Solid.ShippingFast,
- Anchor = Anchor.TopCentre,
- Origin = Anchor.Centre,
};
- labelLate = new SpriteIcon
+ labelLate.Child = new SpriteIcon
{
- Y = 10,
Size = new Vector2(icon_size),
Icon = FontAwesome.Solid.Bicycle,
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.Centre,
};
break;
case LabelStyles.Text:
- labelEarly = new OsuSpriteText
+ labelEarly.Child = new OsuSpriteText
{
- Y = -10,
Text = "Early",
Font = OsuFont.Default.With(size: 10),
Height = 12,
- Anchor = Anchor.TopCentre,
- Origin = Anchor.Centre,
};
- labelLate = new OsuSpriteText
+ labelLate.Child = new OsuSpriteText
{
- Y = 10,
Text = "Late",
Font = OsuFont.Default.With(size: 10),
Height = 12,
- Anchor = Anchor.BottomCentre,
- Origin = Anchor.Centre,
};
break;
@@ -320,26 +319,8 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
throw new ArgumentOutOfRangeException(nameof(style), style, null);
}
- if (labelEarly != null)
- {
- colourBars.Add(labelEarly);
- labelEarly.FadeInFromZero(500);
- }
-
- if (labelLate != null)
- {
- colourBars.Add(labelLate);
- labelLate.FadeInFromZero(500);
- }
- }
-
- protected override void Update()
- {
- base.Update();
-
- // undo any layout rotation to display icons in the correct orientation
- if (labelEarly != null) labelEarly.Rotation = -Rotation;
- if (labelLate != null) labelLate.Rotation = -Rotation;
+ labelEarly.FadeInFromZero(500);
+ labelLate.FadeInFromZero(500);
}
private void createColourBars((HitResult result, double length)[] windows)
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/Player.cs b/osu.Game/Screens/Play/Player.cs
index 68b623b781..7048f83c09 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -66,6 +66,8 @@ namespace osu.Game.Screens.Play
public override bool HideOverlaysOnEnter => true;
+ public override bool HideMenuCursorOnNonMouseInput => true;
+
protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered;
// We are managing our own adjustments (see OnEntering/OnExiting).
diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs
index e32d3d90be..4ff5083107 100644
--- a/osu.Game/Screens/Play/PlayerLoader.cs
+++ b/osu.Game/Screens/Play/PlayerLoader.cs
@@ -64,6 +64,8 @@ namespace osu.Game.Screens.Play
protected Task? DisposalTask { get; private set; }
+ private OsuScrollContainer settingsScroll = null!;
+
private bool backgroundBrightnessReduction;
private readonly BindableDouble volumeAdjustment = new BindableDouble(1);
@@ -71,6 +73,9 @@ namespace osu.Game.Screens.Play
private AudioFilter lowPassFilter = null!;
private AudioFilter highPassFilter = null!;
+ [Cached]
+ private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
+
protected bool BackgroundBrightnessReduction
{
set
@@ -165,30 +170,30 @@ namespace osu.Game.Screens.Play
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
- new OsuScrollContainer
- {
- Anchor = Anchor.TopRight,
- Origin = Anchor.TopRight,
- RelativeSizeAxes = Axes.Y,
- Width = SettingsToolboxGroup.CONTAINER_WIDTH + padding * 2,
- Padding = new MarginPadding { Vertical = padding },
- Masking = false,
- Child = PlayerSettings = new FillFlowContainer
- {
- AutoSizeAxes = Axes.Both,
- Direction = FillDirection.Vertical,
- Spacing = new Vector2(0, 20),
- Padding = new MarginPadding { Horizontal = padding },
- Children = new PlayerSettingsGroup[]
- {
- VisualSettings = new VisualSettings(),
- AudioSettings = new AudioSettings(),
- new InputSettings()
- }
- },
- },
- idleTracker = new IdleTracker(750),
}),
+ settingsScroll = new OsuScrollContainer
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ RelativeSizeAxes = Axes.Y,
+ Width = SettingsToolboxGroup.CONTAINER_WIDTH + padding * 2,
+ Padding = new MarginPadding { Vertical = padding },
+ Masking = false,
+ Child = PlayerSettings = new FillFlowContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Vertical,
+ Spacing = new Vector2(0, 20),
+ Padding = new MarginPadding { Horizontal = padding },
+ Children = new PlayerSettingsGroup[]
+ {
+ VisualSettings = new VisualSettings(),
+ AudioSettings = new AudioSettings(),
+ new InputSettings()
+ }
+ },
+ },
+ idleTracker = new IdleTracker(750),
lowPassFilter = new AudioFilter(audio.TrackMixer),
highPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass)
};
@@ -224,6 +229,9 @@ namespace osu.Game.Screens.Play
Beatmap.Value.Track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment);
+ // Start off-screen.
+ settingsScroll.MoveToX(settingsScroll.DrawWidth);
+
content.ScaleTo(0.7f);
contentIn();
@@ -313,6 +321,16 @@ namespace osu.Game.Screens.Play
content.StopTracking();
}
+ protected override void LogoSuspending(OsuLogo logo)
+ {
+ base.LogoSuspending(logo);
+ content.StopTracking();
+
+ logo
+ .FadeOut(CONTENT_OUT_DURATION / 2, Easing.OutQuint)
+ .ScaleTo(logo.Scale * 0.8f, CONTENT_OUT_DURATION * 2, Easing.OutQuint);
+ }
+
#endregion
protected override void Update()
@@ -391,6 +409,10 @@ namespace osu.Game.Screens.Play
content.FadeInFromZero(400);
content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer);
+
+ settingsScroll.FadeInFromZero(500, Easing.Out)
+ .MoveToX(0, 500, Easing.OutQuint);
+
lowPassFilter.CutoffTo(1000, 650, Easing.OutQuint);
highPassFilter.CutoffTo(300).Then().CutoffTo(0, 1250); // 1250 is to line up with the appearance of MetadataInfo (750 delay + 500 fade-in)
@@ -404,6 +426,10 @@ namespace osu.Game.Screens.Play
content.ScaleTo(0.7f, CONTENT_OUT_DURATION * 2, Easing.OutQuint);
content.FadeOut(CONTENT_OUT_DURATION, Easing.OutQuint);
+
+ settingsScroll.FadeOut(CONTENT_OUT_DURATION, Easing.OutQuint)
+ .MoveToX(settingsScroll.DrawWidth, CONTENT_OUT_DURATION * 2, Easing.OutQuint);
+
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, CONTENT_OUT_DURATION);
highPassFilter.CutoffTo(0, CONTENT_OUT_DURATION);
}
@@ -432,7 +458,7 @@ namespace osu.Game.Screens.Play
ContentOut();
- TransformSequence pushSequence = this.Delay(CONTENT_OUT_DURATION);
+ TransformSequence pushSequence = this.Delay(0);
// only show if the warning was created (i.e. the beatmap needs it)
// and this is not a restart of the map (the warning expires after first load).
@@ -441,6 +467,7 @@ namespace osu.Game.Screens.Play
const double epilepsy_display_length = 3000;
pushSequence
+ .Delay(CONTENT_OUT_DURATION)
.Schedule(() => epilepsyWarning.State.Value = Visibility.Visible)
.TransformBindableTo(volumeAdjustment, 0.25, EpilepsyWarning.FADE_DURATION, Easing.OutQuint)
.Delay(epilepsy_display_length)
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/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs
index d56b9c23c8..345bd5a134 100644
--- a/osu.Game/Screens/Play/SubmittingPlayer.cs
+++ b/osu.Game/Screens/Play/SubmittingPlayer.cs
@@ -86,16 +86,13 @@ namespace osu.Game.Screens.Play
// Generally a timeout would not happen here as APIAccess will timeout first.
if (!tcs.Task.Wait(60000))
- handleTokenFailure(new InvalidOperationException("Token retrieval timed out (request never run)"));
+ req.TriggerFailure(new InvalidOperationException("Token retrieval timed out (request never run)"));
return true;
void handleTokenFailure(Exception exception)
{
- // This method may be invoked multiple times due to the Task.Wait call above.
- // We only really care about the first error.
- if (!tcs.TrySetResult(false))
- return;
+ tcs.SetResult(false);
if (HandleTokenRetrievalFailure(exception))
{
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/Screens/Utility/LatencyArea.cs b/osu.Game/Screens/Utility/LatencyArea.cs
index c8e0bf93a2..b7d45ba642 100644
--- a/osu.Game/Screens/Utility/LatencyArea.cs
+++ b/osu.Game/Screens/Utility/LatencyArea.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Screens.Utility
public readonly Bindable VisualMode = new Bindable();
- public CursorContainer? MenuCursor { get; private set; }
+ public CursorContainer? Cursor { get; private set; }
public bool ProvidingUserCursor => IsActiveArea.Value;
@@ -91,7 +91,7 @@ namespace osu.Game.Screens.Utility
{
RelativeSizeAxes = Axes.Both,
},
- MenuCursor = new LatencyCursorContainer
+ Cursor = new LatencyCursorContainer
{
RelativeSizeAxes = Axes.Both,
},
@@ -105,7 +105,7 @@ namespace osu.Game.Screens.Utility
{
RelativeSizeAxes = Axes.Both,
},
- MenuCursor = new LatencyCursorContainer
+ Cursor = new LatencyCursorContainer
{
RelativeSizeAxes = Axes.Both,
},
@@ -119,7 +119,7 @@ namespace osu.Game.Screens.Utility
{
RelativeSizeAxes = Axes.Both,
},
- MenuCursor = new LatencyCursorContainer
+ Cursor = new LatencyCursorContainer
{
RelativeSizeAxes = Axes.Both,
},
diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs
index 04f1286dc7..b80275a1e8 100644
--- a/osu.Game/Skinning/DefaultLegacySkin.cs
+++ b/osu.Game/Skinning/DefaultLegacySkin.cs
@@ -46,6 +46,8 @@ namespace osu.Game.Skinning
new Color4(242, 24, 57, 255)
};
+ Configuration.ConfigDictionary[nameof(SkinConfiguration.LegacySetting.AllowSliderBallTint)] = @"true";
+
Configuration.LegacyVersion = 2.7m;
}
}
diff --git a/osu.Game/Skinning/Editor/SkinComponentToolbox.cs b/osu.Game/Skinning/Editor/SkinComponentToolbox.cs
index 980dee8601..469657c03c 100644
--- a/osu.Game/Skinning/Editor/SkinComponentToolbox.cs
+++ b/osu.Game/Skinning/Editor/SkinComponentToolbox.cs
@@ -85,10 +85,6 @@ namespace osu.Game.Skinning.Editor
{
public Action? RequestPlacement;
- protected override bool ShouldBeConsideredForInput(Drawable child) => false;
-
- public override bool PropagateNonPositionalInputSubTree => false;
-
private readonly Drawable component;
private readonly CompositeDrawable? dependencySource;
@@ -177,6 +173,10 @@ namespace osu.Game.Skinning.Editor
public class DependencyBorrowingContainer : Container
{
+ protected override bool ShouldBeConsideredForInput(Drawable child) => false;
+
+ public override bool PropagateNonPositionalInputSubTree => false;
+
private readonly CompositeDrawable? donor;
public DependencyBorrowingContainer(CompositeDrawable? donor)
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 0b1159f8fd..a9f660312e 100644
--- a/osu.Game/Skinning/SkinConfiguration.cs
+++ b/osu.Game/Skinning/SkinConfiguration.cs
@@ -38,7 +38,8 @@ namespace osu.Game.Skinning
HitCirclePrefix,
HitCircleOverlap,
AnimationFramerate,
- LayeredHitSounds
+ LayeredHitSounds,
+ AllowSliderBallTint,
}
public static List DefaultComboColours { get; } = new List
@@ -65,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/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs
index e47d19fba6..3ca83a4781 100644
--- a/osu.Game/Tests/Visual/OsuGameTestScene.cs
+++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs
@@ -17,6 +17,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
+using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Overlays;
@@ -42,6 +43,8 @@ namespace osu.Game.Tests.Visual
protected override bool CreateNestedActionContainer => false;
+ protected override bool DisplayCursorForManualInput => false;
+
[BackgroundDependencyLoader]
private void load()
{
@@ -119,6 +122,8 @@ namespace osu.Game.Tests.Visual
public RealmAccess Realm => Dependencies.Get();
+ public new GlobalCursorDisplay GlobalCursorDisplay => base.GlobalCursorDisplay;
+
public new BackButton BackButton => base.BackButton;
public new BeatmapManager BeatmapManager => base.BeatmapManager;
diff --git a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs
index 9082ca9c58..e56c546bac 100644
--- a/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs
+++ b/osu.Game/Tests/Visual/OsuManualInputManagerTestScene.cs
@@ -36,21 +36,31 @@ namespace osu.Game.Tests.Visual
///
protected virtual bool CreateNestedActionContainer => true;
+ ///
+ /// Whether a menu cursor controlled by the manual input manager should be displayed.
+ /// True by default, but is disabled for s as they provide their own global cursor.
+ ///
+ protected virtual bool DisplayCursorForManualInput => true;
+
protected OsuManualInputManagerTestScene()
{
- GlobalCursorDisplay cursorDisplay;
+ var mainContent = content = new Container { RelativeSizeAxes = Axes.Both };
- CompositeDrawable mainContent = cursorDisplay = new GlobalCursorDisplay { RelativeSizeAxes = Axes.Both };
-
- cursorDisplay.Child = content = new OsuTooltipContainer(cursorDisplay.MenuCursor)
+ if (DisplayCursorForManualInput)
{
- RelativeSizeAxes = Axes.Both
- };
+ var cursorDisplay = new GlobalCursorDisplay { RelativeSizeAxes = Axes.Both };
+
+ cursorDisplay.Add(new OsuTooltipContainer(cursorDisplay.MenuCursor)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = mainContent
+ });
+
+ mainContent = cursorDisplay;
+ }
if (CreateNestedActionContainer)
- {
mainContent = new GlobalActionContainer(null).WithChild(mainContent);
- }
base.Content.AddRange(new Drawable[]
{
diff --git a/osu.Game/Tests/Visual/ScrollingTestContainer.cs b/osu.Game/Tests/Visual/ScrollingTestContainer.cs
index cf7fe6e45d..1817a704b9 100644
--- a/osu.Game/Tests/Visual/ScrollingTestContainer.cs
+++ b/osu.Game/Tests/Visual/ScrollingTestContainer.cs
@@ -99,8 +99,8 @@ namespace osu.Game.Tests.Visual
public float GetLength(double startTime, double endTime, double timeRange, float scrollLength)
=> implementation.GetLength(startTime, endTime, timeRange, scrollLength);
- public float PositionAt(double time, double currentTime, double timeRange, float scrollLength)
- => implementation.PositionAt(time, currentTime, timeRange, scrollLength);
+ public float PositionAt(double time, double currentTime, double timeRange, float scrollLength, double? originTime = null)
+ => implementation.PositionAt(time, currentTime, timeRange, scrollLength, originTime);
public double TimeAt(float position, double currentTime, double timeRange, float scrollLength)
=> implementation.TimeAt(position, currentTime, timeRange, scrollLength);
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index f1fed6913b..8d45ebec57 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -18,7 +18,7 @@
-
+
@@ -35,8 +35,8 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
diff --git a/osu.iOS.props b/osu.iOS.props
index c79d0e4864..76d2e727c8 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -61,8 +61,8 @@
-
-
+
+
@@ -82,7 +82,7 @@
-
+