diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 1f937e1837..d9f1e3985a 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "jetbrains.resharper.globaltools": { - "version": "2022.2.3", + "version": "2022.3.3", "commands": [ "jb" ] diff --git a/Directory.Build.props b/Directory.Build.props index 734374c840..fccac79a45 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@  - 10.0 + 11.0 true enable diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs index 5f22ef5c12..1e63d32c41 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs @@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints private double placementStartTime; private double placementEndTime; + protected override bool IsValidForPlacement => HitObject.Duration > 0; + public BananaShowerPlacementBlueprint() { InternalChild = outline = new TimeSpanOutline(); @@ -49,7 +51,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints case PlacementState.Active: if (e.Button != MouseButton.Right) break; - EndPlacement(HitObject.Duration > 0); + EndPlacement(true); return true; } diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs index 03ec674abb..9e50b5a80f 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs @@ -24,6 +24,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints private InputManager inputManager = null!; + protected override bool IsValidForPlacement => HitObject.Duration > 0; + public JuiceStreamPlacementBlueprint() { InternalChildren = new Drawable[] @@ -70,7 +72,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints return true; case MouseButton.Right: - EndPlacement(HitObject.Duration > 0); + EndPlacement(true); return true; } diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs index 21beee0769..381af8be7f 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs @@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints [Resolved] private IScrollingInfo scrollingInfo { get; set; } + protected override bool IsValidForPlacement => HitObject.Duration > 0; + public HoldNotePlacementBlueprint() : base(new HoldNote()) { @@ -75,7 +77,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints return; base.OnMouseUp(e); - EndPlacement(HitObject.Duration > 0); + EndPlacement(true); } private double originalStartTime; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index 50514865e1..28ceb80627 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -41,6 +41,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders [Resolved(CanBeNull = true)] private IDistanceSnapProvider snapProvider { get; set; } + protected override bool IsValidForPlacement => HitObject.Path.HasValidLength; + public SliderPlacementBlueprint() : base(new Slider()) { @@ -150,7 +152,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private void endCurve() { updateSlider(); - EndPlacement(HitObject.Path.HasValidLength); + EndPlacement(true); } protected override void Update() diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs index 307d731fd4..3224ff9eaf 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs @@ -9,7 +9,6 @@ using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; -using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Beatmaps; @@ -28,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModTargetPractice)).ToArray(); - [SettingSource("Angle sharpness", "How sharp angles should be", SettingControlType = typeof(SettingsSlider))] + [SettingSource("Angle sharpness", "How sharp angles should be")] public BindableFloat AngleSharpness { get; } = new BindableFloat(7) { MinValue = 1, diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs index fcf2573d64..bc4129c982 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs @@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints private readonly IHasDuration spanPlacementObject; + protected override bool IsValidForPlacement => spanPlacementObject.Duration > 0; + public TaikoSpanPlacementBlueprint(HitObject hitObject) : base(hitObject) { @@ -73,7 +75,7 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints return; base.OnMouseUp(e); - EndPlacement(spanPlacementObject.Duration > 0); + EndPlacement(true); } public override void UpdateTimeAndPosition(SnapResult result) diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs new file mode 100644 index 0000000000..a5681bea4a --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestScenePlacementBlueprint.cs @@ -0,0 +1,106 @@ +// 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.Screens; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Input.Bindings; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.UI; +using osu.Game.Screens.Edit.Compose.Components; +using osu.Game.Tests.Beatmaps; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Editing +{ + public partial class TestScenePlacementBlueprint : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); + + private GlobalActionContainer globalActionContainer => this.ChildrenOfType().Single(); + + [Test] + public void TestCommitPlacementViaGlobalAction() + { + Playfield playfield = null!; + + AddStep("select slider placement tool", () => InputManager.Key(Key.Number3)); + AddStep("move mouse to top left of playfield", () => + { + playfield = this.ChildrenOfType().Single(); + var location = (3 * playfield.ScreenSpaceDrawQuad.TopLeft + playfield.ScreenSpaceDrawQuad.BottomRight) / 4; + InputManager.MoveMouseTo(location); + }); + AddStep("begin placement", () => InputManager.Click(MouseButton.Left)); + AddStep("move mouse to bottom right of playfield", () => + { + var location = (playfield.ScreenSpaceDrawQuad.TopLeft + 3 * playfield.ScreenSpaceDrawQuad.BottomRight) / 4; + InputManager.MoveMouseTo(location); + }); + AddStep("confirm via global action", () => + { + globalActionContainer.TriggerPressed(GlobalAction.Select); + globalActionContainer.TriggerReleased(GlobalAction.Select); + }); + AddAssert("slider placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1)); + } + + [Test] + public void TestAbortPlacementViaGlobalAction() + { + Playfield playfield = null!; + + AddStep("select slider placement tool", () => InputManager.Key(Key.Number3)); + AddStep("move mouse to top left of playfield", () => + { + playfield = this.ChildrenOfType().Single(); + var location = (3 * playfield.ScreenSpaceDrawQuad.TopLeft + playfield.ScreenSpaceDrawQuad.BottomRight) / 4; + InputManager.MoveMouseTo(location); + }); + AddStep("begin placement", () => InputManager.Click(MouseButton.Left)); + AddStep("move mouse to bottom right of playfield", () => + { + var location = (playfield.ScreenSpaceDrawQuad.TopLeft + 3 * playfield.ScreenSpaceDrawQuad.BottomRight) / 4; + InputManager.MoveMouseTo(location); + }); + AddStep("abort via global action", () => + { + globalActionContainer.TriggerPressed(GlobalAction.Back); + globalActionContainer.TriggerReleased(GlobalAction.Back); + }); + AddAssert("editor is still current", () => Editor.IsCurrentScreen()); + AddAssert("slider not placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(0)); + AddAssert("no active placement", () => this.ChildrenOfType().Single().CurrentPlacement.PlacementActive, + () => Is.EqualTo(PlacementBlueprint.PlacementState.Waiting)); + } + + [Test] + public void TestCommitPlacementViaToolChange() + { + Playfield playfield = null!; + + AddStep("select slider placement tool", () => InputManager.Key(Key.Number3)); + AddStep("move mouse to top left of playfield", () => + { + playfield = this.ChildrenOfType().Single(); + var location = (3 * playfield.ScreenSpaceDrawQuad.TopLeft + playfield.ScreenSpaceDrawQuad.BottomRight) / 4; + InputManager.MoveMouseTo(location); + }); + AddStep("begin placement", () => InputManager.Click(MouseButton.Left)); + AddStep("move mouse to bottom right of playfield", () => + { + var location = (playfield.ScreenSpaceDrawQuad.TopLeft + 3 * playfield.ScreenSpaceDrawQuad.BottomRight) / 4; + InputManager.MoveMouseTo(location); + }); + + AddStep("change tool to circle", () => InputManager.Key(Key.Number2)); + AddAssert("slider placed", () => EditorBeatmap.HitObjects.Count, () => Is.EqualTo(1)); + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index f3ff2e87ba..b76b9a40f9 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -239,7 +239,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics { Scheduler.AddOnce(d => { - displayDropdown.Items = d; + if (!displayDropdown.Items.SequenceEqual(d, DisplayListComparer.DEFAULT)) + displayDropdown.Items = d; updateDisplaySettingsVisibility(); }, displays); } @@ -371,5 +372,43 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics } } } + + /// + /// Contrary to , this comparer disregards the value of . + /// We want to just show a list of displays, and for the purposes of settings we don't care about their bounds when it comes to the list. + /// However, fires even if only the resolution of the current display was changed + /// (because it causes the bounds of all displays to also change). + /// We're not interested in those changes, so compare only the rest that we actually care about. + /// This helps to avoid a bindable/event feedback loop, in which a resolution change + /// would trigger a display "change", which would in turn reset resolution again. + /// + private class DisplayListComparer : IEqualityComparer + { + public static readonly DisplayListComparer DEFAULT = new DisplayListComparer(); + + public bool Equals(Display? x, Display? y) + { + if (ReferenceEquals(x, y)) return true; + if (ReferenceEquals(x, null)) return false; + if (ReferenceEquals(y, null)) return false; + + return x.Index == y.Index + && x.Name == y.Name + && x.DisplayModes.SequenceEqual(y.DisplayModes); + } + + public int GetHashCode(Display obj) + { + var hashCode = new HashCode(); + + hashCode.Add(obj.Index); + hashCode.Add(obj.Name); + hashCode.Add(obj.DisplayModes.Length); + foreach (var displayMode in obj.DisplayModes) + hashCode.Add(displayMode); + + return hashCode.ToHashCode(); + } + } } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index 951cf3802f..4c9320c2a6 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -3,6 +3,8 @@ #nullable disable +using System.Collections.Generic; +using System.Linq; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -23,6 +25,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input { public partial class TabletSettings : SettingsSubsection { + public override IEnumerable FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "area" }); + public TabletAreaSelection AreaSelection { get; private set; } private readonly ITabletHandler tabletHandler; diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index bdcb334738..12c0ea1807 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -9,10 +9,12 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Input.Bindings; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose; @@ -24,7 +26,7 @@ namespace osu.Game.Rulesets.Edit /// /// A blueprint which governs the creation of a new to actualisation. /// - public abstract partial class PlacementBlueprint : CompositeDrawable + public abstract partial class PlacementBlueprint : CompositeDrawable, IKeyBindingHandler { /// /// Whether the is currently mid-placement, but has not necessarily finished being placed. @@ -47,6 +49,15 @@ namespace osu.Game.Rulesets.Edit [Resolved] private IPlacementHandler placementHandler { get; set; } + /// + /// Whether this blueprint is currently in a state that can be committed. + /// + /// + /// Override this with any preconditions that should be double-checked on committing. + /// If false is returned and a commit is attempted, the blueprint will be destroyed instead. + /// + protected virtual bool IsValidForPlacement => true; + protected PlacementBlueprint(HitObject hitObject) { HitObject = hitObject; @@ -88,7 +99,7 @@ namespace osu.Game.Rulesets.Edit /// Signals that the placement of has finished. /// This will destroy this , and add the HitObject.StartTime to the . /// - /// Whether the object should be committed. + /// Whether the object should be committed. Note that a commit may fail if is false. public void EndPlacement(bool commit) { switch (PlacementActive) @@ -102,10 +113,34 @@ namespace osu.Game.Rulesets.Edit break; } - placementHandler.EndPlacement(HitObject, commit); + placementHandler.EndPlacement(HitObject, IsValidForPlacement && commit); PlacementActive = PlacementState.Finished; } + public bool OnPressed(KeyBindingPressEvent e) + { + if (PlacementActive == PlacementState.Waiting) + return false; + + switch (e.Action) + { + case GlobalAction.Select: + EndPlacement(true); + return true; + + case GlobalAction.Back: + EndPlacement(false); + return true; + + default: + return false; + } + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + /// /// Updates the time and position of this based on the provided snap information. /// diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 453e4b9130..07fb450628 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -317,12 +317,16 @@ namespace osu.Game.Screens.Edit.Compose.Components } } + private void commitIfPlacementActive() + { + CurrentPlacement?.EndPlacement(CurrentPlacement.PlacementActive == PlacementBlueprint.PlacementState.Active); + removePlacement(); + } + private void removePlacement() { - if (CurrentPlacement == null) return; - - CurrentPlacement.EndPlacement(false); - CurrentPlacement.Expire(); + CurrentPlacement?.EndPlacement(false); + CurrentPlacement?.Expire(); CurrentPlacement = null; } @@ -342,7 +346,8 @@ namespace osu.Game.Screens.Edit.Compose.Components currentTool = value; - refreshTool(); + // As per stable editor, when changing tools, we should forcefully commit any pending placement. + commitIfPlacementActive(); } } } diff --git a/osu.Game/Screens/Play/SquareGraph.cs b/osu.Game/Screens/Play/SquareGraph.cs index 57b7c84e89..b53e86a41b 100644 --- a/osu.Game/Screens/Play/SquareGraph.cs +++ b/osu.Game/Screens/Play/SquareGraph.cs @@ -15,6 +15,7 @@ using osuTK; using osuTK.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Allocation; +using osu.Framework.Layout; using osu.Framework.Threading; namespace osu.Game.Screens.Play @@ -51,7 +52,7 @@ namespace osu.Game.Screens.Play if (value == values) return; values = value; - graphNeedsUpdate = true; + layout.Invalidate(); } } @@ -71,23 +72,25 @@ namespace osu.Game.Screens.Play private ScheduledDelegate scheduledCreate; - private bool graphNeedsUpdate; + private readonly LayoutValue layout = new LayoutValue(Invalidation.DrawSize | Invalidation.DrawInfo); - private Vector2 previousDrawSize; + public SquareGraph() + { + AddLayout(layout); + } protected override void Update() { base.Update(); - if (graphNeedsUpdate || (values != null && DrawSize != previousDrawSize)) + if (!layout.IsValid) { columns?.FadeOut(500, Easing.OutQuint).Expire(); scheduledCreate?.Cancel(); scheduledCreate = Scheduler.AddDelayed(RecreateGraph, 500); - previousDrawSize = DrawSize; - graphNeedsUpdate = false; + layout.Validate(); } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 3ea4a57c2c..bf0c1dde17 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -1,9 +1,8 @@ - + net6.0 Library true - 10 osu! diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index d7486273fb..9ca0fad2ab 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -234,6 +234,8 @@ WARNING WARNING HINT + + DO_NOT_SHOW WARNING HINT HINT @@ -329,6 +331,7 @@ False AABB API + ARGB BPM EF FPS @@ -336,6 +339,7 @@ GL GLSL HID + HSL HSPA HSV HTML @@ -352,6 +356,7 @@ OS PM RGB + RGBA RNG SDL SHA