diff --git a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs index 9ff6d10a49..630a2cf645 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchHitObjectComposer.cs @@ -89,9 +89,9 @@ namespace osu.Game.Rulesets.Catch.Edit new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler }) }); - public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) + public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) { - var result = base.SnapScreenSpacePositionToValidTime(screenSpacePosition); + var result = base.FindSnappedPositionAndTime(screenSpacePosition); result.ScreenSpacePosition.X = screenSpacePosition.X; if (distanceSnapGrid.IsPresent && distanceSnapGrid.GetSnappedPosition(result.ScreenSpacePosition) is SnapResult snapResult && diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs index 2e55f86bb6..4bb049b1a4 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs @@ -97,12 +97,12 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor set => InternalChild = value; } - public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) + public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) { throw new System.NotImplementedException(); } - public override SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) + public override SnapResult FindSnappedPosition(Vector2 screenSpacePosition) { throw new System.NotImplementedException(); } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 2baec95c94..fef315e2ef 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -56,9 +56,9 @@ namespace osu.Game.Rulesets.Mania.Edit protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => Playfield.GetColumnByPosition(screenSpacePosition); - public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) + public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) { - var result = base.SnapScreenSpacePositionToValidTime(screenSpacePosition); + var result = base.FindSnappedPositionAndTime(screenSpacePosition); switch (ScrollingInfo.Direction.Value) { @@ -112,7 +112,7 @@ namespace osu.Game.Rulesets.Mania.Edit } else { - var result = SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position); + var result = FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position); if (result.Time is double time) beatSnapGrid.SelectionTimeRange = (time, time); else diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs index 2ba30c5f74..368166157d 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs @@ -182,10 +182,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private class SnapProvider : IDistanceSnapProvider { - public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) => + public SnapResult FindSnappedPosition(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, null); - public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0); + public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0); public IBindable DistanceSpacingMultiplier { get; } = new BindableDouble(1); @@ -195,9 +195,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor public double DistanceToDuration(HitObject referenceObject, float distance) => distance; - public double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => 0; + public double FindSnappedDuration(HitObject referenceObject, float distance) => 0; - public float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) => 0; + public float FindSnappedDistance(HitObject referenceObject, float distance) => 0; } } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 5de614722f..e71bde7357 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -255,7 +255,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { // Special handling for selections containing head control point - the position of the slider changes which means the snapped position and time have to be taken into account Vector2 newHeadPosition = Parent.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex])); - var result = snapProvider?.SnapScreenSpacePositionToValidTime(newHeadPosition); + var result = snapProvider?.FindSnappedPositionAndTime(newHeadPosition); Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - slider.Position; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 73a4ea5434..501589987d 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -220,7 +220,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private void updateSlider() { - HitObject.Path.ExpectedDistance.Value = snapProvider?.GetSnappedDistanceFromDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance; + HitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance; bodyPiece.UpdateFrom(HitObject); headCirclePiece.UpdateFrom(HitObject.HeadCircle); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index ba7c6e9d33..02beb0f2a4 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -123,7 +123,7 @@ namespace osu.Game.Rulesets.Osu.Edit } } - public override SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) + public override SnapResult FindSnappedPosition(Vector2 screenSpacePosition) { if (snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) return snapResult; @@ -131,9 +131,9 @@ namespace osu.Game.Rulesets.Osu.Edit return new SnapResult(screenSpacePosition, null); } - public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) + public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) { - var positionSnap = SnapScreenSpacePositionToValidPosition(screenSpacePosition); + var positionSnap = FindSnappedPosition(screenSpacePosition); if (positionSnap.ScreenSpacePosition != screenSpacePosition) return positionSnap; @@ -149,7 +149,7 @@ namespace osu.Game.Rulesets.Osu.Edit return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition)); } - return base.SnapScreenSpacePositionToValidTime(screenSpacePosition); + return base.FindSnappedPositionAndTime(screenSpacePosition); } private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs index dfa28a537a..0832cfb545 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAlternate.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Acronym => @"AL"; public override string Description => @"Don't use the same key twice in a row!"; public override double ScoreMultiplier => 1.0; - public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay) }; + public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax) }; public override ModType Type => ModType.Conversion; public override IconUsage? Icon => FontAwesome.Solid.Keyboard; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs index b31ef5d2fd..507b3588bd 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModAutoplay : ModAutoplay { - public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray(); + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate) }).ToArray(); public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) => new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" }); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs index 5b42772358..99d7535957 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModCinema : ModCinema { - public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray(); + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate) }).ToArray(); public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList mods) => new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" }); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 6b81efdca6..5f37c6a0ae 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset, IApplicableToPlayer { public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things."; - public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised) }).ToArray(); + public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModAlternate) }).ToArray(); /// /// How early before a hitobject's start time to trigger a hit. diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs index ab45e5192d..22ace52c2a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs @@ -4,7 +4,6 @@ using System; using System.Linq; using System.Threading; -using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; @@ -23,9 +22,8 @@ namespace osu.Game.Rulesets.Osu.Mods { public override string Name => @"Strict Tracking"; public override string Acronym => @"ST"; - public override IconUsage? Icon => FontAwesome.Solid.PenFancy; public override ModType Type => ModType.DifficultyIncrease; - public override string Description => @"Follow circles just got serious..."; + public override string 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) }; diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index a2ee97210a..82fc7a208b 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -213,10 +213,10 @@ namespace osu.Game.Tests.Editing => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceToDuration(new HitObject(), distance) == expectedDuration); private void assertSnappedDuration(float distance, double expectedDuration) - => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.GetSnappedDurationFromDistance(new HitObject(), distance) == expectedDuration); + => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.FindSnappedDuration(new HitObject(), distance) == expectedDuration); private void assertSnappedDistance(float distance, float expectedDistance) - => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.GetSnappedDistanceFromDistance(new HitObject(), distance) == expectedDistance); + => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.FindSnappedDistance(new HitObject(), distance) == expectedDistance); private class TestHitObjectComposer : OsuHitObjectComposer { diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index b9cfa84a5d..38c0808a71 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -162,10 +162,10 @@ namespace osu.Game.Tests.Visual.Editing private class SnapProvider : IDistanceSnapProvider { - public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) => + public SnapResult FindSnappedPosition(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, null); - public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0); + public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0); public IBindable DistanceSpacingMultiplier { get; } = new BindableDouble(1); @@ -175,9 +175,9 @@ namespace osu.Game.Tests.Visual.Editing public double DistanceToDuration(HitObject referenceObject, float distance) => distance; - public double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => 0; + public double FindSnappedDuration(HitObject referenceObject, float distance) => 0; - public float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) => 0; + public float FindSnappedDistance(HitObject referenceObject, float distance) => 0; } } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs index ae1b691767..c9d44fdab7 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneHitObjectComposer.cs @@ -19,6 +19,7 @@ using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components.RadioButtons; +using osu.Game.Screens.Edit.Components.TernaryButtons; using osu.Game.Screens.Edit.Compose.Components; using osuTK; using osuTK.Input; @@ -70,6 +71,11 @@ namespace osu.Game.Tests.Visual.Editing Child = editorBeatmapContainer = new EditorBeatmapContainer(Beatmap.Value) { Child = hitObjectComposer = new OsuHitObjectComposer(new OsuRuleset()) + { + // force the composer to fully overlap the playfield area by setting a 4:3 aspect ratio. + FillMode = FillMode.Fit, + FillAspectRatio = 4 / 3f + } }; }); } @@ -87,6 +93,65 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("Tool changed", () => hitObjectComposer.ChildrenOfType().First().CurrentTool is HitCircleCompositionTool); } + [Test] + public void TestPlacementFailsWhenClickingButton() + { + AddStep("clear all control points and hitobjects", () => + { + editorBeatmap.ControlPointInfo.Clear(); + editorBeatmap.Clear(); + }); + + AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint())); + + AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType().First(d => d.Button.Label == "HitCircle").TriggerClick()); + + AddStep("move mouse to overlapping toggle button", () => + { + var playfield = hitObjectComposer.Playfield.ScreenSpaceDrawQuad; + var button = hitObjectComposer + .ChildrenOfType().First() + .ChildrenOfType().First(b => playfield.Contains(b.ScreenSpaceDrawQuad.Centre)); + + InputManager.MoveMouseTo(button); + }); + + AddAssert("no circles placed", () => editorBeatmap.HitObjects.Count == 0); + + AddStep("attempt place circle", () => InputManager.Click(MouseButton.Left)); + + AddAssert("no circles placed", () => editorBeatmap.HitObjects.Count == 0); + } + + [Test] + public void TestPlacementWithinToolboxScrollArea() + { + AddStep("clear all control points and hitobjects", () => + { + editorBeatmap.ControlPointInfo.Clear(); + editorBeatmap.Clear(); + }); + + AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint())); + + AddStep("Change to hitcircle", () => hitObjectComposer.ChildrenOfType().First(d => d.Button.Label == "HitCircle").TriggerClick()); + + AddStep("move mouse to scroll area", () => + { + // Specifically wanting to test the area of overlap between the left expanding toolbox container + // and the playfield/composer. + var scrollArea = hitObjectComposer.ChildrenOfType().First().ScreenSpaceDrawQuad; + var playfield = hitObjectComposer.Playfield.ScreenSpaceDrawQuad; + InputManager.MoveMouseTo(new Vector2(scrollArea.TopLeft.X + 1, playfield.Centre.Y)); + }); + + AddAssert("no circles placed", () => editorBeatmap.HitObjects.Count == 0); + + AddStep("place circle", () => InputManager.Click(MouseButton.Left)); + + AddAssert("circle placed", () => editorBeatmap.HitObjects.Count == 1); + } + [Test] public void TestDistanceSpacingHotkeys() { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs index ec6e962c6a..bdb423a43c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs @@ -2,17 +2,21 @@ // 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.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Mods; +using osu.Game.Overlays.Settings; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osuTK.Input; @@ -64,6 +68,7 @@ namespace osu.Game.Tests.Visual.UserInterface return Precision.AlmostEquals(multiplier, modSelectScreen.ChildrenOfType().Single().Current.Value); }); assertCustomisationToggleState(disabled: false, active: false); + AddAssert("setting items created", () => modSelectScreen.ChildrenOfType().Any()); } [Test] @@ -78,6 +83,7 @@ namespace osu.Game.Tests.Visual.UserInterface return Precision.AlmostEquals(multiplier, modSelectScreen.ChildrenOfType().Single().Current.Value); }); assertCustomisationToggleState(disabled: false, active: false); + AddAssert("setting items created", () => modSelectScreen.ChildrenOfType().Any()); } [Test] @@ -98,17 +104,25 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("activate DT", () => getPanelForMod(typeof(OsuModDoubleTime)).TriggerClick()); AddAssert("DT active", () => SelectedMods.Value.Single().GetType() == typeof(OsuModDoubleTime)); + AddAssert("DT panel active", () => getPanelForMod(typeof(OsuModDoubleTime)).Active.Value); AddStep("activate NC", () => getPanelForMod(typeof(OsuModNightcore)).TriggerClick()); AddAssert("only NC active", () => SelectedMods.Value.Single().GetType() == typeof(OsuModNightcore)); + AddAssert("DT panel not active", () => !getPanelForMod(typeof(OsuModDoubleTime)).Active.Value); + AddAssert("NC panel active", () => getPanelForMod(typeof(OsuModNightcore)).Active.Value); AddStep("activate HR", () => getPanelForMod(typeof(OsuModHardRock)).TriggerClick()); AddAssert("NC+HR active", () => SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModNightcore)) && SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModHardRock))); + AddAssert("NC panel active", () => getPanelForMod(typeof(OsuModNightcore)).Active.Value); + AddAssert("HR panel active", () => getPanelForMod(typeof(OsuModHardRock)).Active.Value); AddStep("activate MR", () => getPanelForMod(typeof(OsuModMirror)).TriggerClick()); AddAssert("NC+MR active", () => SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModNightcore)) && SelectedMods.Value.Any(mod => mod.GetType() == typeof(OsuModMirror))); + AddAssert("NC panel active", () => getPanelForMod(typeof(OsuModNightcore)).Active.Value); + AddAssert("HR panel not active", () => !getPanelForMod(typeof(OsuModHardRock)).Active.Value); + AddAssert("MR panel active", () => getPanelForMod(typeof(OsuModMirror)).Active.Value); } [Test] @@ -169,6 +183,206 @@ namespace osu.Game.Tests.Visual.UserInterface assertCustomisationToggleState(disabled: true, active: false); // config was dismissed without explicit user action. } + /// + /// Ensure that two mod overlays are not cross polluting via central settings instances. + /// + [Test] + public void TestSettingsNotCrossPolluting() + { + Bindable> selectedMods2 = null; + ModSelectScreen modSelectScreen2 = null; + + createScreen(); + AddStep("select diff adjust", () => SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() }); + + AddStep("set setting", () => modSelectScreen.ChildrenOfType>().First().Current.Value = 8); + + AddAssert("ensure setting is propagated", () => SelectedMods.Value.OfType().Single().CircleSize.Value == 8); + + AddStep("create second bindable", () => selectedMods2 = new Bindable>(new Mod[] { new OsuModDifficultyAdjust() })); + + AddStep("create second overlay", () => + { + Add(modSelectScreen2 = new UserModSelectScreen().With(d => + { + d.Origin = Anchor.TopCentre; + d.Anchor = Anchor.TopCentre; + d.SelectedMods.BindTarget = selectedMods2; + })); + }); + + AddStep("show", () => modSelectScreen2.Show()); + + AddAssert("ensure first is unchanged", () => SelectedMods.Value.OfType().Single().CircleSize.Value == 8); + AddAssert("ensure second is default", () => selectedMods2.Value.OfType().Single().CircleSize.Value == null); + } + + [Test] + public void TestSettingsResetOnDeselection() + { + var osuModDoubleTime = new OsuModDoubleTime { SpeedChange = { Value = 1.2 } }; + + createScreen(); + changeRuleset(0); + + AddStep("set dt mod with custom rate", () => { SelectedMods.Value = new[] { osuModDoubleTime }; }); + + AddAssert("selected mod matches", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.Value == 1.2); + + AddStep("deselect", () => getPanelForMod(typeof(OsuModDoubleTime)).TriggerClick()); + AddAssert("selected mods empty", () => SelectedMods.Value.Count == 0); + + AddStep("reselect", () => getPanelForMod(typeof(OsuModDoubleTime)).TriggerClick()); + AddAssert("selected mod has default value", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.IsDefault == true); + } + + [Test] + public void TestAnimationFlushOnClose() + { + createScreen(); + changeRuleset(0); + + AddStep("Select all fun mods", () => + { + modSelectScreen.ChildrenOfType() + .Single(c => c.ModType == ModType.DifficultyIncrease) + .SelectAll(); + }); + + AddUntilStep("many mods selected", () => SelectedMods.Value.Count >= 5); + + AddStep("trigger deselect and close overlay", () => + { + modSelectScreen.ChildrenOfType() + .Single(c => c.ModType == ModType.DifficultyIncrease) + .DeselectAll(); + + modSelectScreen.Hide(); + }); + + AddAssert("all mods deselected", () => SelectedMods.Value.Count == 0); + } + + [Test] + public void TestRulesetChanges() + { + createScreen(); + changeRuleset(0); + + var noFailMod = new OsuRuleset().GetModsFor(ModType.DifficultyReduction).FirstOrDefault(m => m is OsuModNoFail); + + AddStep("set mods externally", () => { SelectedMods.Value = new[] { noFailMod }; }); + + changeRuleset(0); + + AddAssert("ensure mods still selected", () => SelectedMods.Value.SingleOrDefault(m => m is OsuModNoFail) != null); + + changeRuleset(3); + + AddAssert("ensure mods not selected", () => SelectedMods.Value.Count == 0); + + changeRuleset(0); + + AddAssert("ensure mods not selected", () => SelectedMods.Value.Count == 0); + } + + [Test] + public void TestExternallySetCustomizedMod() + { + createScreen(); + changeRuleset(0); + + AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } }); + + AddAssert("ensure button is selected and customized accordingly", () => + { + var button = getPanelForMod(SelectedMods.Value.Single().GetType()); + return ((OsuModDoubleTime)button.Mod).SpeedChange.Value == 1.01; + }); + } + + [Test] + public void TestSettingsAreRetainedOnReload() + { + createScreen(); + changeRuleset(0); + + AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } }); + AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01); + + createScreen(); + AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01); + } + + [Test] + public void TestExternallySetModIsReplacedByOverlayInstance() + { + Mod external = new OsuModDoubleTime(); + Mod overlayButtonMod = null; + + createScreen(); + changeRuleset(0); + + AddStep("set mod externally", () => { SelectedMods.Value = new[] { external }; }); + + AddAssert("ensure button is selected", () => + { + var button = getPanelForMod(SelectedMods.Value.Single().GetType()); + overlayButtonMod = button.Mod; + return button.Active.Value; + }); + + // Right now, when an external change occurs, the ModSelectOverlay will replace the global instance with its own + AddAssert("mod instance doesn't match", () => external != overlayButtonMod); + + AddAssert("one mod present in global selected", () => SelectedMods.Value.Count == 1); + AddAssert("globally selected matches button's mod instance", () => SelectedMods.Value.Any(mod => ReferenceEquals(mod, overlayButtonMod))); + AddAssert("globally selected doesn't contain original external change", () => !SelectedMods.Value.Any(mod => ReferenceEquals(mod, external))); + } + + [Test] + public void TestChangeIsValidChangesButtonVisibility() + { + createScreen(); + changeRuleset(0); + + AddAssert("double time visible", () => modSelectScreen.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value)); + + AddStep("make double time invalid", () => modSelectScreen.IsValidMod = m => !(m is OsuModDoubleTime)); + AddUntilStep("double time not visible", () => modSelectScreen.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).All(panel => panel.Filtered.Value)); + AddAssert("nightcore still visible", () => modSelectScreen.ChildrenOfType().Where(panel => panel.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value)); + + AddStep("make double time valid again", () => modSelectScreen.IsValidMod = m => true); + AddUntilStep("double time visible", () => modSelectScreen.ChildrenOfType().Where(panel => panel.Mod is OsuModDoubleTime).Any(panel => !panel.Filtered.Value)); + AddAssert("nightcore still visible", () => modSelectScreen.ChildrenOfType().Where(b => b.Mod is OsuModNightcore).Any(panel => !panel.Filtered.Value)); + } + + [Test] + public void TestChangeIsValidPreservesSelection() + { + createScreen(); + changeRuleset(0); + + AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() }); + AddAssert("DT + HD selected", () => modSelectScreen.ChildrenOfType().Count(panel => panel.Active.Value) == 2); + + AddStep("make NF invalid", () => modSelectScreen.IsValidMod = m => !(m is ModNoFail)); + AddAssert("DT + HD still selected", () => modSelectScreen.ChildrenOfType().Count(panel => panel.Active.Value) == 2); + } + + [Test] + public void TestUnimplementedModIsUnselectable() + { + var testRuleset = new TestUnimplementedModOsuRuleset(); + + createScreen(); + + AddStep("set ruleset", () => Ruleset.Value = testRuleset.RulesetInfo); + waitForColumnLoad(); + + AddAssert("unimplemented mod panel is filtered", () => getPanelForMod(typeof(TestUnimplementedMod)).Filtered.Value); + } + private void waitForColumnLoad() => AddUntilStep("all column content loaded", () => modSelectScreen.ChildrenOfType().Any() && modSelectScreen.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded)); @@ -188,5 +402,26 @@ namespace osu.Game.Tests.Visual.UserInterface private ModPanel getPanelForMod(Type modType) => modSelectScreen.ChildrenOfType().Single(panel => panel.Mod.GetType() == modType); + + private class TestUnimplementedMod : Mod + { + public override string Name => "Unimplemented mod"; + public override string Acronym => "UM"; + public override string Description => "A mod that is not implemented."; + public override double ScoreMultiplier => 1; + public override ModType Type => ModType.Conversion; + } + + private class TestUnimplementedModOsuRuleset : OsuRuleset + { + public override string ShortName => "unimplemented"; + + public override IEnumerable GetModsFor(ModType type) + { + if (type == ModType.Conversion) return base.GetModsFor(type).Concat(new[] { new TestUnimplementedMod() }); + + return base.GetModsFor(type); + } + } } } diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 1a9703f478..abc9020dc6 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -1,21 +1,23 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using System.Linq; using JetBrains.Annotations; using Newtonsoft.Json; using osu.Framework.Testing; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Database; using osu.Game.Models; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.BeatmapSet.Scores; using osu.Game.Rulesets; +using osu.Game.Rulesets.Edit; using osu.Game.Scoring; using Realms; -#nullable enable - namespace osu.Game.Beatmaps { /// @@ -109,6 +111,16 @@ namespace osu.Game.Beatmaps public bool SamplesMatchPlaybackRate { get; set; } = true; + /// + /// The ratio of distance travelled per time unit. + /// Generally used to decouple the spacing between hit objects from the enforced "velocity" of the beatmap (see ). + /// + /// + /// The most common method of understanding is that at a default value of 1.0, the time-to-distance ratio will match the slider velocity of the beatmap + /// at the current point in time. Increasing this value will make hit objects more spaced apart when compared to the cursor movement required to track a slider. + /// + /// This is only a hint property, used by the editor in implementations. It does not directly affect the beatmap or gameplay. + /// public double DistanceSpacing { get; set; } = 1.0; public int BeatDivisor { get; set; } diff --git a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs index a24b6b315a..46f5b418bd 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs @@ -153,7 +153,7 @@ namespace osu.Game.Beatmaps } }; - cacheDownloadRequest.PerformAsync(); + Task.Run(() => cacheDownloadRequest.PerformAsync()); } private bool checkLocalCache(BeatmapSetInfo set, BeatmapInfo beatmapInfo) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 3e7051cbf5..13a3f006fb 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -59,6 +59,9 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.Up, GlobalAction.SelectPrevious), new KeyBinding(InputKey.Down, GlobalAction.SelectNext), + new KeyBinding(InputKey.Left, GlobalAction.SelectPreviousGroup), + new KeyBinding(InputKey.Right, GlobalAction.SelectNextGroup), + new KeyBinding(InputKey.Space, GlobalAction.Select), new KeyBinding(InputKey.Enter, GlobalAction.Select), new KeyBinding(InputKey.KeypadEnter, GlobalAction.Select), @@ -105,7 +108,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.F1, GlobalAction.ToggleModSelection), new KeyBinding(InputKey.F2, GlobalAction.SelectNextRandom), new KeyBinding(new[] { InputKey.Shift, InputKey.F2 }, GlobalAction.SelectPreviousRandom), - new KeyBinding(InputKey.F3, GlobalAction.ToggleBeatmapOptions) + new KeyBinding(InputKey.F3, GlobalAction.ToggleBeatmapOptions), }; public IEnumerable AudioControlKeyBindings => new[] @@ -309,5 +312,11 @@ namespace osu.Game.Input.Bindings [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorDecreaseDistanceSpacing))] EditorDecreaseDistanceSpacing, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.SelectPreviousGroup))] + SelectPreviousGroup, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.SelectNextGroup))] + SelectNextGroup, } } diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 58afb284b5..b2f25de7f2 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -129,6 +129,16 @@ namespace osu.Game.Localisation /// public static LocalisableString SelectNext => new TranslatableString(getKey(@"select_next"), @"Next selection"); + /// + /// "Select previous group" + /// + public static LocalisableString SelectPreviousGroup => new TranslatableString(getKey(@"select_previous_group"), @"Select previous group"); + + /// + /// "Select next group" + /// + public static LocalisableString SelectNextGroup => new TranslatableString(getKey(@"select_next_group"), @"Select next group"); + /// /// "Home" /// diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index 018922c074..a792c0a81e 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using System.Collections.Generic; using System.Linq; @@ -10,6 +12,7 @@ using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -25,8 +28,6 @@ using osuTK; using osuTK.Graphics; using osuTK.Input; -#nullable enable - namespace osu.Game.Overlays.Mods { public class ModColumn : CompositeDrawable @@ -52,9 +53,22 @@ namespace osu.Game.Overlays.Mods } } - public Bindable> SelectedMods = new Bindable>(Array.Empty()); public Bindable Active = new BindableBool(true); + /// + /// List of mods marked as selected in this column. + /// + /// + /// Note that the mod instances returned by this property are owned solely by this column + /// (as in, they are locally-managed clones, to ensure proper isolation from any other external instances). + /// + public IReadOnlyList SelectedMods { get; private set; } = Array.Empty(); + + /// + /// Invoked when a mod panel has been selected interactively by the user. + /// + public event Action? SelectionChangedByUser; + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && Active.Value; protected virtual ModPanel CreateModPanel(Mod mod) => new ModPanel(mod); @@ -63,6 +77,15 @@ namespace osu.Game.Overlays.Mods private readonly Bindable>> availableMods = new Bindable>>(); + /// + /// All mods that are available for the current ruleset in this particular column. + /// + /// + /// Note that the mod instances in this list are owned solely by this column + /// (as in, they are locally-managed clones, to ensure proper isolation from any other external instances). + /// + private IReadOnlyList localAvailableMods = Array.Empty(); + private readonly TextFlowContainer headerText; private readonly Box headerBackground; private readonly Container contentContainer; @@ -226,6 +249,9 @@ namespace osu.Game.Overlays.Mods private void load(OsuGameBase game, OverlayColourProvider colourProvider, OsuColour colours) { availableMods.BindTo(game.AvailableMods); + // this `BindValueChanged` callback is intentionally here, to ensure that local available mods are constructed as early as possible. + // this is needed to make sure no external changes to mods are dropped while mod panels are asynchronously loading. + availableMods.BindValueChanged(_ => updateLocalAvailableMods(), true); headerBackground.Colour = accentColour = colours.ForModType(ModType); @@ -239,31 +265,26 @@ namespace osu.Game.Overlays.Mods contentBackground.Colour = colourProvider.Background4; } - protected override void LoadComplete() + private void updateLocalAvailableMods() { - base.LoadComplete(); - availableMods.BindValueChanged(_ => Scheduler.AddOnce(updateMods)); - SelectedMods.BindValueChanged(_ => - { - // if a load is in progress, don't try to update the selection - the load flow will do so. - if (latestLoadTask == null) - updateActiveState(); - }); - updateMods(); + var newMods = ModUtils.FlattenMods(availableMods.Value.GetValueOrDefault(ModType) ?? Array.Empty()) + .Select(m => m.DeepClone()) + .ToList(); + + if (newMods.SequenceEqual(localAvailableMods)) + return; + + localAvailableMods = newMods; + Scheduler.AddOnce(loadPanels); } private CancellationTokenSource? cancellationTokenSource; - private void updateMods() + private void loadPanels() { - var newMods = ModUtils.FlattenMods(availableMods.Value.GetValueOrDefault(ModType) ?? Array.Empty()).ToList(); - - if (newMods.SequenceEqual(panelFlow.Children.Select(p => p.Mod))) - return; - cancellationTokenSource?.Cancel(); - var panels = newMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0))); + var panels = localAvailableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0))); Task? loadTask; @@ -277,13 +298,7 @@ namespace osu.Game.Overlays.Mods foreach (var panel in panelFlow) { - panel.Active.BindValueChanged(_ => - { - updateToggleAllState(); - SelectedMods.Value = panel.Active.Value - ? SelectedMods.Value.Append(panel.Mod).ToArray() - : SelectedMods.Value.Except(new[] { panel.Mod }).ToArray(); - }); + panel.Active.BindValueChanged(_ => panelStateChanged(panel)); } }, (cancellationTokenSource = new CancellationTokenSource()).Token); loadTask.ContinueWith(_ => @@ -296,7 +311,62 @@ namespace osu.Game.Overlays.Mods private void updateActiveState() { foreach (var panel in panelFlow) - panel.Active.Value = SelectedMods.Value.Contains(panel.Mod, EqualityComparer.Default); + panel.Active.Value = SelectedMods.Contains(panel.Mod); + } + + /// + /// This flag helps to determine the source of changes to . + /// If the value is false, then are changing due to a user selection on the UI. + /// If the value is true, then are changing due to an external call. + /// + private bool externalSelectionUpdateInProgress; + + private void panelStateChanged(ModPanel panel) + { + updateToggleAllState(); + + var newSelectedMods = panel.Active.Value + ? SelectedMods.Append(panel.Mod) + : SelectedMods.Except(panel.Mod.Yield()); + + SelectedMods = newSelectedMods.ToArray(); + if (!externalSelectionUpdateInProgress) + SelectionChangedByUser?.Invoke(); + } + + /// + /// Adjusts the set of selected mods in this column to match the passed in . + /// + /// + /// This method exists to be able to receive mod instances that come from potentially-external sources and to copy the changes across to this column's state. + /// uses this to substitute any external mod references in + /// to references that are owned by this column. + /// + internal void SetSelection(IReadOnlyList mods) + { + externalSelectionUpdateInProgress = true; + + var newSelection = new List(); + + foreach (var mod in localAvailableMods) + { + var matchingSelectedMod = mods.SingleOrDefault(selected => selected.GetType() == mod.GetType()); + + if (matchingSelectedMod != null) + { + mod.CopyFrom(matchingSelectedMod); + newSelection.Add(mod); + } + else + { + mod.ResetSettingsToDefaults(); + } + } + + SelectedMods = newSelection; + updateActiveState(); + + externalSelectionUpdateInProgress = false; } #region Bulk select / deselect @@ -364,6 +434,15 @@ namespace osu.Game.Overlays.Mods pendingSelectionOperations.Enqueue(() => button.Active.Value = false); } + /// + /// Run any delayed selections (due to animation) immediately to leave mods in a good (final) state. + /// + public void FlushPendingSelections() + { + while (pendingSelectionOperations.TryDequeue(out var dequeuedAction)) + dequeuedAction(); + } + private class ToggleAllCheckbox : OsuCheckbox { private Color4 accentColour; diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index 5bf8cddd0c..a70191a864 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -250,9 +250,9 @@ namespace osu.Game.Overlays.Mods protected virtual ModButton CreateModButton(Mod mod) => new ModButton(mod); /// - /// Play out all remaining animations immediately to leave mods in a good (final) state. + /// Run any delayed selections (due to animation) immediately to leave mods in a good (final) state. /// - public void FlushAnimation() + public void FlushPendingSelections() { while (pendingSelectionOperations.TryDequeue(out var dequeuedAction)) dequeuedAction(); diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 9ce79c25f7..cf57322594 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -369,7 +369,7 @@ namespace osu.Game.Overlays.Mods foreach (var section in ModSectionsContainer) { - section.FlushAnimation(); + section.FlushPendingSelections(); } FooterContainer.MoveToX(content_width, WaveContainer.DISAPPEAR_DURATION, Easing.InSine); diff --git a/osu.Game/Overlays/Mods/ModSelectScreen.cs b/osu.Game/Overlays/Mods/ModSelectScreen.cs index ffd6e9a52c..8060bca65f 100644 --- a/osu.Game/Overlays/Mods/ModSelectScreen.cs +++ b/osu.Game/Overlays/Mods/ModSelectScreen.cs @@ -179,7 +179,7 @@ namespace osu.Game.Overlays.Mods foreach (var column in columnFlow.Columns) { - column.SelectedMods.BindValueChanged(updateBindableFromSelection); + column.SelectionChangedByUser += updateBindableFromSelection; } customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true); @@ -203,7 +203,7 @@ namespace osu.Game.Overlays.Mods private void updateAvailableMods() { foreach (var column in columnFlow.Columns) - column.Filter = isValidMod; + column.Filter = m => m.HasImplementation && isValidMod.Invoke(m); } private void updateCustomisation(ValueChangedEvent> valueChangedEvent) @@ -250,33 +250,26 @@ namespace osu.Game.Overlays.Mods private void updateSelectionFromBindable() { - // note that selectionBindableSyncInProgress is purposefully not checked here. - // this is because in the case of mod selection in solo gameplay, a user selection of a mod can actually lead to deselection of other incompatible mods. - // to synchronise state correctly, updateBindableFromSelection() computes the final mods (including incompatibility rules) and updates SelectedMods, - // and this method then runs unconditionally again to make sure the new visual selection accurately reflects the final set of selected mods. - // selectionBindableSyncInProgress ensures that mutual infinite recursion does not happen after that unconditional call. + // `SelectedMods` may contain mod references that come from external sources. + // to ensure isolation, first pull in the potentially-external change into the mod columns... foreach (var column in columnFlow.Columns) - column.SelectedMods.Value = SelectedMods.Value.Where(mod => mod.Type == column.ModType).ToArray(); + column.SetSelection(SelectedMods.Value); + + // and then, when done, replace the potentially-external mod references in `SelectedMods` with ones we own. + updateBindableFromSelection(); } - private bool selectionBindableSyncInProgress; - - private void updateBindableFromSelection(ValueChangedEvent> modSelectionChange) + private void updateBindableFromSelection() { - if (selectionBindableSyncInProgress) + var candidateSelection = columnFlow.Columns.SelectMany(column => column.SelectedMods).ToArray(); + + if (candidateSelection.SequenceEqual(SelectedMods.Value)) return; - selectionBindableSyncInProgress = true; - - SelectedMods.Value = ComputeNewModsFromSelection( - modSelectionChange.NewValue.Except(modSelectionChange.OldValue), - modSelectionChange.OldValue.Except(modSelectionChange.NewValue)); - - selectionBindableSyncInProgress = false; + SelectedMods.Value = ComputeNewModsFromSelection(SelectedMods.Value, candidateSelection); } - protected virtual IReadOnlyList ComputeNewModsFromSelection(IEnumerable addedMods, IEnumerable removedMods) - => columnFlow.Columns.SelectMany(column => column.SelectedMods.Value).ToArray(); + protected virtual IReadOnlyList ComputeNewModsFromSelection(IReadOnlyList oldSelection, IReadOnlyList newSelection) => newSelection; protected override void PopIn() { @@ -313,10 +306,12 @@ namespace osu.Game.Overlays.Mods { const float distance = 700; - columnFlow[i].Column - .TopLevelContent - .MoveToY(i % 2 == 0 ? -distance : distance, fade_out_duration, Easing.OutQuint) - .FadeOut(fade_out_duration, Easing.OutQuint); + var column = columnFlow[i].Column; + + column.FlushPendingSelections(); + column.TopLevelContent + .MoveToY(i % 2 == 0 ? -distance : distance, fade_out_duration, Easing.OutQuint) + .FadeOut(fade_out_duration, Easing.OutQuint); } } diff --git a/osu.Game/Overlays/Mods/ModSettingsArea.cs b/osu.Game/Overlays/Mods/ModSettingsArea.cs index be72c1e3e3..9c5f3b7f11 100644 --- a/osu.Game/Overlays/Mods/ModSettingsArea.cs +++ b/osu.Game/Overlays/Mods/ModSettingsArea.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -21,7 +22,7 @@ namespace osu.Game.Overlays.Mods { public class ModSettingsArea : CompositeDrawable { - public Bindable> SelectedMods { get; } = new Bindable>(); + public Bindable> SelectedMods { get; } = new Bindable>(Array.Empty()); public const float HEIGHT = 250; @@ -77,7 +78,7 @@ namespace osu.Game.Overlays.Mods protected override void LoadComplete() { base.LoadComplete(); - SelectedMods.BindValueChanged(_ => updateMods()); + SelectedMods.BindValueChanged(_ => updateMods(), true); } private void updateMods() diff --git a/osu.Game/Overlays/Mods/UserModSelectScreen.cs b/osu.Game/Overlays/Mods/UserModSelectScreen.cs index ed0a07521b..ca33d35605 100644 --- a/osu.Game/Overlays/Mods/UserModSelectScreen.cs +++ b/osu.Game/Overlays/Mods/UserModSelectScreen.cs @@ -14,9 +14,12 @@ namespace osu.Game.Overlays.Mods { protected override ModColumn CreateModColumn(ModType modType, Key[] toggleKeys = null) => new UserModColumn(modType, false, toggleKeys); - protected override IReadOnlyList ComputeNewModsFromSelection(IEnumerable addedMods, IEnumerable removedMods) + protected override IReadOnlyList ComputeNewModsFromSelection(IReadOnlyList oldSelection, IReadOnlyList newSelection) { - IEnumerable modsAfterRemoval = SelectedMods.Value.Except(removedMods).ToList(); + var addedMods = newSelection.Except(oldSelection); + var removedMods = oldSelection.Except(newSelection); + + IEnumerable modsAfterRemoval = newSelection.Except(removedMods).ToList(); // the preference is that all new mods should override potential incompatible old mods. // in general that's a bit difficult to compute if more than one mod is added at a time, diff --git a/osu.Game/Overlays/Volume/VolumeMeter.cs b/osu.Game/Overlays/Volume/VolumeMeter.cs index e2afd46c18..46b8b35da2 100644 --- a/osu.Game/Overlays/Volume/VolumeMeter.cs +++ b/osu.Game/Overlays/Volume/VolumeMeter.cs @@ -372,12 +372,12 @@ namespace osu.Game.Overlays.Volume switch (e.Action) { - case GlobalAction.SelectPrevious: + case GlobalAction.SelectPreviousGroup: State = SelectionState.Selected; adjust(1, false); return true; - case GlobalAction.SelectNext: + case GlobalAction.SelectNextGroup: State = SelectionState.Selected; adjust(-1, false); return true; diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs index 081610a62b..bbcb702bd8 100644 --- a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs @@ -10,14 +10,12 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Configuration; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Overlays.OSD; using osu.Game.Overlays.Settings.Sections; using osu.Game.Rulesets.Objects; -using osuTK; namespace osu.Game.Rulesets.Edit { @@ -25,7 +23,6 @@ namespace osu.Game.Rulesets.Edit /// Represents a for rulesets with the concept of distances between objects. /// /// The base type of supported objects. - [Cached(typeof(IDistanceSnapProvider))] public abstract class DistancedHitObjectComposer : HitObjectComposer, IDistanceSnapProvider, IScrollBindingHandler where TObject : HitObject { @@ -53,8 +50,9 @@ namespace osu.Game.Rulesets.Edit [BackgroundDependencyLoader] private void load() { - AddInternal(RightSideToolboxContainer = new ExpandingToolboxContainer + AddInternal(RightSideToolboxContainer = new ExpandingToolboxContainer(130, 250) { + Padding = new MarginPadding { Right = 10 }, Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, @@ -147,10 +145,10 @@ namespace osu.Game.Rulesets.Edit return distance / GetBeatSnapDistanceAt(referenceObject) * beatLength; } - public virtual double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) + public virtual double FindSnappedDuration(HitObject referenceObject, float distance) => BeatSnapProvider.SnapTime(referenceObject.StartTime + DistanceToDuration(referenceObject, distance), referenceObject.StartTime) - referenceObject.StartTime; - public virtual float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) + public virtual float FindSnappedDistance(HitObject referenceObject, float distance) { double startTime = referenceObject.StartTime; @@ -168,20 +166,6 @@ namespace osu.Game.Rulesets.Edit return DurationToDistance(referenceObject, snappedEndTime - startTime); } - protected class ExpandingToolboxContainer : ExpandingContainer - { - protected override double HoverExpansionDelay => 250; - - public ExpandingToolboxContainer() - : base(130, 250) - { - RelativeSizeAxes = Axes.Y; - Padding = new MarginPadding { Left = 10 }; - - FillFlow.Spacing = new Vector2(10); - } - } - private class DistanceSpacingToast : Toast { private readonly ValueChangedEvent change; diff --git a/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs new file mode 100644 index 0000000000..e807dbd482 --- /dev/null +++ b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs @@ -0,0 +1,34 @@ +// 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 osu.Framework.Graphics; +using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; +using osuTK; + +namespace osu.Game.Rulesets.Edit +{ + public class ExpandingToolboxContainer : ExpandingContainer + { + protected override double HoverExpansionDelay => 250; + + public ExpandingToolboxContainer(float contractedWidth, float expandedWidth) + : base(contractedWidth, expandedWidth) + { + RelativeSizeAxes = Axes.Y; + + FillFlow.Spacing = new Vector2(10); + } + + protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && anyToolboxHovered(screenSpacePos); + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) && anyToolboxHovered(screenSpacePos); + + private bool anyToolboxHovered(Vector2 screenSpacePos) => FillFlow.Children.Any(d => d.ScreenSpaceDrawQuad.Contains(screenSpacePos)); + + protected override bool OnMouseDown(MouseDownEvent e) => true; + + protected override bool OnClick(ClickEvent e) => true; + } +} diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index a235a5bc60..216510fcf3 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -13,7 +13,6 @@ using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Game.Beatmaps; -using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; @@ -36,7 +35,6 @@ namespace osu.Game.Rulesets.Edit /// Responsible for providing snapping and generally gluing components together. /// /// The base type of supported objects. - [Cached(Type = typeof(IPlacementHandler))] public abstract class HitObjectComposer : HitObjectComposer, IPlacementHandler where TObject : HitObject { @@ -115,8 +113,9 @@ namespace osu.Game.Rulesets.Edit .WithChild(BlueprintContainer = CreateBlueprintContainer()) } }, - new LeftToolboxFlow + new ExpandingToolboxContainer(80, 200) { + Padding = new MarginPadding { Left = 10 }, Children = new Drawable[] { new EditorToolboxGroup("toolbox (1-9)") @@ -362,7 +361,7 @@ namespace osu.Game.Rulesets.Edit /// The most relevant . protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield; - public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) + public override SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) { var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); double? targetTime = null; @@ -382,26 +381,13 @@ namespace osu.Game.Rulesets.Edit } #endregion - - private class LeftToolboxFlow : ExpandingButtonContainer - { - public LeftToolboxFlow() - : base(80, 200) - { - RelativeSizeAxes = Axes.Y; - Padding = new MarginPadding { Right = 10 }; - - FillFlow.Spacing = new Vector2(10); - } - } } /// /// A non-generic definition of a HitObject composer class. /// Generally used to access certain methods without requiring a generic type for . /// - [Cached(typeof(HitObjectComposer))] - [Cached(typeof(IPositionSnapProvider))] + [Cached] public abstract class HitObjectComposer : CompositeDrawable, IPositionSnapProvider { protected HitObjectComposer() @@ -428,9 +414,9 @@ namespace osu.Game.Rulesets.Edit #region IPositionSnapProvider - public abstract SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition); + public abstract SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition); - public virtual SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) => + public virtual SnapResult FindSnappedPosition(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, null); #endregion diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs index c6e866561e..8c599f8596 100644 --- a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs @@ -1,16 +1,21 @@ // 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.Game.Beatmaps; using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Edit { + /// + /// A snap provider which given a reference hit object and proposed distance from it, offers a more correct duration or distance value. + /// + [Cached] public interface IDistanceSnapProvider : IPositionSnapProvider { /// - /// The spacing multiplier applied to beat snap distances. + /// A multiplier which changes the ratio of distance travelled per time unit. /// /// IBindable DistanceSpacingMultiplier { get; } @@ -23,7 +28,7 @@ namespace osu.Game.Rulesets.Edit float GetBeatSnapDistanceAt(HitObject referenceObject); /// - /// Converts a duration to a distance. + /// Converts a duration to a distance without applying any snapping. /// /// An object to be used as a reference point for this operation. /// The duration to convert. @@ -31,7 +36,7 @@ namespace osu.Game.Rulesets.Edit float DurationToDistance(HitObject referenceObject, double duration); /// - /// Converts a distance to a duration. + /// Converts a distance to a duration without applying any snapping. /// /// An object to be used as a reference point for this operation. /// The distance to convert. @@ -39,20 +44,22 @@ namespace osu.Game.Rulesets.Edit double DistanceToDuration(HitObject referenceObject, float distance); /// - /// Converts a distance to a snapped duration. + /// Given a distance from the provided hit object, find the valid snapped duration. /// /// An object to be used as a reference point for this operation. /// The distance to convert. /// A value that represents as a duration snapped to the closest beat of the timing point. - double GetSnappedDurationFromDistance(HitObject referenceObject, float distance); + double FindSnappedDuration(HitObject referenceObject, float distance); /// - /// Converts an unsnapped distance to a snapped distance. - /// The returned distance will always be floored (as to never exceed the provided . + /// Given a distance from the provided hit object, find the valid snapped distance. /// /// An object to be used as a reference point for this operation. /// The distance to convert. - /// A value that represents snapped to the closest beat of the timing point. - float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance); + /// + /// A value that represents snapped to the closest beat of the timing point. + /// The distance will always be less than or equal to the provided . + /// + float FindSnappedDistance(HitObject referenceObject, float distance); } } diff --git a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs index 8a179ed424..837b04424a 100644 --- a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs @@ -1,27 +1,33 @@ // 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 osuTK; namespace osu.Game.Rulesets.Edit { + /// + /// A snap provider which given a proposed position for a hit object, potentially offers a more correct position and time value inferred from the context of the beatmap. + /// Provided values are inferred in an isolated context, without consideration of other nearby hit objects. + /// + [Cached] public interface IPositionSnapProvider { /// /// Given a position, find a valid time and position snap. /// /// - /// This call should be equivalent to running with any additional logic that can be performed without the time immutability restriction. + /// This call should be equivalent to running with any additional logic that can be performed without the time immutability restriction. /// /// The screen-space position to be snapped. /// The time and position post-snapping. - SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition); + SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition); /// - /// Given a position, find a value position snap, restricting time to its input value. + /// Given a position, find a valid position snap, without changing the time value. /// /// The screen-space position to be snapped. /// The position post-snapping. Time will always be null. - SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition); + SnapResult FindSnappedPosition(Vector2 screenSpacePosition); } } diff --git a/osu.Game/Rulesets/Edit/ScrollingToolboxGroup.cs b/osu.Game/Rulesets/Edit/ScrollingToolboxGroup.cs deleted file mode 100644 index 98e026c49a..0000000000 --- a/osu.Game/Rulesets/Edit/ScrollingToolboxGroup.cs +++ /dev/null @@ -1,34 +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 osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics.Containers; - -namespace osu.Game.Rulesets.Edit -{ - public class ScrollingToolboxGroup : EditorToolboxGroup - { - protected readonly OsuScrollContainer Scroll; - - protected readonly FillFlowContainer FillFlow; - - protected override Container Content { get; } - - public ScrollingToolboxGroup(string title, float scrollAreaHeight) - : base(title) - { - base.Content.Add(Scroll = new OsuScrollContainer - { - RelativeSizeAxes = Axes.X, - Height = scrollAreaHeight, - Child = Content = FillFlow = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - }, - }); - } - } -} diff --git a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs index 3100d26a55..dd418a1b7b 100644 --- a/osu.Game/Rulesets/Objects/SliderPathExtensions.cs +++ b/osu.Game/Rulesets/Objects/SliderPathExtensions.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Objects public static void SnapTo(this THitObject hitObject, IDistanceSnapProvider? snapProvider) where THitObject : HitObject, IHasPath { - hitObject.Path.ExpectedDistance.Value = snapProvider?.GetSnappedDistanceFromDistance(hitObject, (float)hitObject.Path.CalculatedDistance) ?? hitObject.Path.CalculatedDistance; + hitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance(hitObject, (float)hitObject.Path.CalculatedDistance) ?? hitObject.Path.CalculatedDistance; } /// diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 6dc6f20cfe..186c66e0af 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -486,7 +486,7 @@ namespace osu.Game.Screens.Edit.Compose.Components Vector2 originalPosition = movementBlueprintOriginalPositions[i]; var testPosition = originalPosition + distanceTravelled; - var positionalResult = snapProvider.SnapScreenSpacePositionToValidPosition(testPosition); + var positionalResult = snapProvider.FindSnappedPosition(testPosition); if (positionalResult.ScreenSpacePosition == testPosition) continue; @@ -505,7 +505,7 @@ namespace osu.Game.Screens.Edit.Compose.Components Vector2 movePosition = movementBlueprintOriginalPositions.First() + distanceTravelled; // Retrieve a snapped position. - var result = snapProvider?.SnapScreenSpacePositionToValidTime(movePosition); + var result = snapProvider?.FindSnappedPositionAndTime(movePosition); if (result == null) { diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs index 6b32ff96c4..50d5f0389a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs @@ -80,7 +80,7 @@ namespace osu.Game.Screens.Edit.Compose.Components Vector2 normalisedDirection = direction * new Vector2(1f / distance); Vector2 snappedPosition = StartPosition + normalisedDirection * radialCount * radius; - return (snappedPosition, StartTime + SnapProvider.GetSnappedDurationFromDistance(ReferenceObject, (snappedPosition - StartPosition).Length)); + return (snappedPosition, StartTime + SnapProvider.FindSnappedDuration(ReferenceObject, (snappedPosition - StartPosition).Length)); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 79b38861ee..68be20720d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -214,7 +214,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void updatePlacementPosition() { - var snapResult = Composer.SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position); + var snapResult = Composer.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position); // if no time was found from positional snapping, we should still quantize to the beat. snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index c2b2bdb861..51b8792d87 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -19,7 +19,6 @@ using osuTK; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { - [Cached(typeof(IPositionSnapProvider))] [Cached] public class Timeline : ZoomableScrollContainer, IPositionSnapProvider { @@ -307,10 +306,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// public double VisibleRange => track.Length / Zoom; - public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) => + public SnapResult FindSnappedPosition(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, null); - public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => + public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition)))); private double getTimeFromPosition(Vector2 localPosition) => diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 6426d33e99..33ea137d51 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -382,7 +382,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { OnDragHandled?.Invoke(e); - if (timeline.SnapScreenSpacePositionToValidTime(e.ScreenSpaceMousePosition).Time is double time) + if (timeline.FindSnappedPositionAndTime(e.ScreenSpaceMousePosition).Time is double time) { switch (hitObject) { diff --git a/osu.Game/Screens/Edit/Compose/IPlacementHandler.cs b/osu.Game/Screens/Edit/Compose/IPlacementHandler.cs index aefcbc6542..59eb13cae5 100644 --- a/osu.Game/Screens/Edit/Compose/IPlacementHandler.cs +++ b/osu.Game/Screens/Edit/Compose/IPlacementHandler.cs @@ -1,10 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Game.Rulesets.Objects; namespace osu.Game.Screens.Edit.Compose { + [Cached] public interface IPlacementHandler { /// diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs index c85a4fc38b..5a7a60b479 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.OnlinePlay public new Func IsValidMod { get => base.IsValidMod; - set => base.IsValidMod = m => m.HasImplementation && m.UserPlayable && value.Invoke(m); + set => base.IsValidMod = m => m.UserPlayable && value.Invoke(m); } public FreeModSelectScreen() diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index c3d340ac61..a59f14647d 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -604,34 +604,20 @@ namespace osu.Game.Screens.Select public void ScrollToSelected(bool immediate = false) => pendingScrollOperation = immediate ? PendingScrollOperation.Immediate : PendingScrollOperation.Standard; - #region Key / button selection logic - - protected override bool OnKeyDown(KeyDownEvent e) - { - switch (e.Key) - { - case Key.Left: - SelectNext(-1); - return true; - - case Key.Right: - SelectNext(); - return true; - } - - return false; - } + #region Button selection logic public bool OnPressed(KeyBindingPressEvent e) { switch (e.Action) { case GlobalAction.SelectNext: - SelectNext(1, false); + case GlobalAction.SelectNextGroup: + SelectNext(1, e.Action == GlobalAction.SelectNextGroup); return true; case GlobalAction.SelectPrevious: - SelectNext(-1, false); + case GlobalAction.SelectPreviousGroup: + SelectNext(-1, e.Action == GlobalAction.SelectPreviousGroup); return true; } diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs index b2f5b1754f..b981a31bd1 100644 --- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs @@ -14,7 +14,6 @@ using osu.Game.Screens.Edit.Compose; namespace osu.Game.Tests.Visual { - [Cached(Type = typeof(IPlacementHandler))] public abstract class PlacementBlueprintTestScene : OsuManualInputManagerTestScene, IPlacementHandler { protected readonly Container HitObjectContainer;