diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 5a3eadf607..1132396608 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -2,12 +2,6 @@ "version": 1, "isRoot": true, "tools": { - "dotnet-format": { - "version": "3.1.37601", - "commands": [ - "dotnet-format" - ] - }, "jetbrains.resharper.globaltools": { "version": "2022.1.0-eap10", "commands": [ diff --git a/.editorconfig b/.editorconfig index 840fa98334..c0ea55f4c8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,6 +1,14 @@ # EditorConfig is awesome: http://editorconfig.org root = true +[*.{csproj,props,targets}] +charset = utf-8-bom +end_of_line = crlf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + [*.cs] end_of_line = crlf insert_final_newline = true @@ -8,8 +16,19 @@ indent_style = space indent_size = 4 trim_trailing_whitespace = true +#license header +file_header_template = Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\nSee the LICENCE file in the repository root for full licence text. + #Roslyn naming styles +#PascalCase for public and protected members +dotnet_naming_style.pascalcase.capitalization = pascal_case +dotnet_naming_symbols.public_members.applicable_accessibilities = public,internal,protected,protected_internal,private_protected +dotnet_naming_symbols.public_members.applicable_kinds = property,method,field,event +dotnet_naming_rule.public_members_pascalcase.severity = error +dotnet_naming_rule.public_members_pascalcase.symbols = public_members +dotnet_naming_rule.public_members_pascalcase.style = pascalcase + #camelCase for private members dotnet_naming_style.camelcase.capitalization = camel_case @@ -172,24 +191,11 @@ csharp_style_prefer_index_operator = false:silent csharp_style_prefer_range_operator = false:silent csharp_style_prefer_switch_expression = false:none -#Supressing roslyn built-in analyzers -# Suppress: EC112 - -#Private method is unused -dotnet_diagnostic.IDE0051.severity = silent -#Private member is unused -dotnet_diagnostic.IDE0052.severity = silent - -#Rules for disposable -dotnet_diagnostic.IDE0067.severity = none -dotnet_diagnostic.IDE0068.severity = none -dotnet_diagnostic.IDE0069.severity = none - -#Disable operator overloads requiring alternate named methods -dotnet_diagnostic.CA2225.severity = none - -# Banned APIs -dotnet_diagnostic.RS0030.severity = error +[*.{yaml,yml}] +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true dotnet_diagnostic.OLOC001.words_in_name = 5 dotnet_diagnostic.OLOC001.license_header = // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.\n// See the LICENCE file in the repository root for full licence text. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2066f27de..514acef525 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,9 @@ jobs: path: ${{ github.workspace }}/inspectcode key: inspectcode-${{ hashFiles('.config/dotnet-tools.json') }}-${{ hashFiles('.github/workflows/ci.yml' ) }} + - name: Dotnet code style + run: dotnet build -c Debug -warnaserror osu.Desktop.slnf -p:EnforceCodeStyleInBuild=true + - name: CodeFileSanity run: | # TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround. @@ -46,10 +49,6 @@ jobs: done <<< $(dotnet codefilesanity) exit $exit_code - # Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded. - # - name: .NET Format (Dry Run) - # run: dotnet format --dry-run --check - - name: InspectCode run: dotnet jb inspectcode $(pwd)/osu.Desktop.slnf --no-build --output="inspectcodereport.xml" --caches-home="inspectcode" --verbosity=WARN @@ -147,4 +146,4 @@ jobs: # cannot accept .sln(f) files as arguments. # Build just the main game for now. - name: Build - run: msbuild osu.iOS/osu.iOS.csproj /restore /p:Configuration=Debug \ No newline at end of file + run: msbuild osu.iOS/osu.iOS.csproj /restore /p:Configuration=Debug diff --git a/.globalconfig b/.globalconfig new file mode 100644 index 0000000000..462dbc74ed --- /dev/null +++ b/.globalconfig @@ -0,0 +1,55 @@ +is_global = true + +# .NET Code Style +# IDE styles reference: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ + +# IDE0001: Simplify names +dotnet_diagnostic.IDE0001.severity = warning + +# IDE0002: Simplify member access +dotnet_diagnostic.IDE0002.severity = warning + +# IDE0003: Remove qualification +dotnet_diagnostic.IDE0003.severity = warning + +# IDE0004: Remove unnecessary cast +dotnet_diagnostic.IDE0004.severity = warning + +# IDE0005: Remove unnecessary imports +dotnet_diagnostic.IDE0005.severity = warning + +# IDE0034: Simplify default literal +dotnet_diagnostic.IDE0034.severity = warning + +# IDE0036: Sort modifiers +dotnet_diagnostic.IDE0036.severity = warning + +# IDE0040: Add accessibility modifier +dotnet_diagnostic.IDE0040.severity = warning + +# IDE0049: Use keyword for type name +dotnet_diagnostic.IDE0040.severity = warning + +# IDE0055: Fix formatting +dotnet_diagnostic.IDE0055.severity = warning + +# IDE0051: Private method is unused +dotnet_diagnostic.IDE0051.severity = silent + +# IDE0052: Private member is unused +dotnet_diagnostic.IDE0052.severity = silent + +# IDE0073: File header +dotnet_diagnostic.IDE0073.severity = warning + +# IDE0130: Namespace mismatch with folder +dotnet_diagnostic.IDE0130.severity = warning + +# IDE1006: Naming style +dotnet_diagnostic.IDE1006.severity = warning + +#Disable operator overloads requiring alternate named methods +dotnet_diagnostic.CA2225.severity = none + +# Banned APIs +dotnet_diagnostic.RS0030.severity = error diff --git a/Directory.Build.props b/Directory.Build.props index 709545bf1d..73a150d3e3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,4 +1,4 @@ - + 8.0 diff --git a/osu.Android.props b/osu.Android.props index b6260fd1d4..2866ec24a6 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -1,4 +1,4 @@ - + 8.0 bin\$(Configuration) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs index e345e03c96..88fd3b36ba 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs @@ -29,13 +29,14 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor protected CatchSelectionBlueprintTestScene() { - EditorBeatmap = new EditorBeatmap(new CatchBeatmap + var catchBeatmap = new CatchBeatmap { BeatmapInfo = { Ruleset = new CatchRuleset().RulesetInfo, } - }) { Difficulty = { CircleSize = 0 } }; + }; + EditorBeatmap = new EditorBeatmap(catchBeatmap) { Difficulty = { CircleSize = 0 } }; EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 100 diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs index 368166157d..3c3c5cb939 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs @@ -4,15 +4,13 @@ using System; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; +using osu.Framework.Input; using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Objects; @@ -25,19 +23,29 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { public class TestSceneOsuDistanceSnapGrid : OsuManualInputManagerTestScene { - private const double beat_length = 100; + private const float beat_length = 100; + private static readonly Vector2 grid_position = new Vector2(512, 384); [Cached(typeof(EditorBeatmap))] + [Cached(typeof(IBeatSnapProvider))] private readonly EditorBeatmap editorBeatmap; + [Cached] + private readonly EditorClock editorClock; + [Cached] private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); [Cached(typeof(IDistanceSnapProvider))] - private readonly SnapProvider snapProvider = new SnapProvider(); + private readonly OsuHitObjectComposer snapProvider = new OsuHitObjectComposer(new OsuRuleset()) + { + // Just used for the snap implementation, so let's hide from vision. + AlwaysPresent = true, + Alpha = 0, + }; - private TestOsuDistanceSnapGrid grid; + private OsuDistanceSnapGrid grid; public TestSceneOsuDistanceSnapGrid() { @@ -48,14 +56,25 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor Ruleset = new OsuRuleset().RulesetInfo } }); + + editorClock = new EditorClock(editorBeatmap); + + base.Content.Children = new Drawable[] + { + snapProvider, + Content + }; } + protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; + [SetUp] public void Setup() => Schedule(() => { editorBeatmap.Difficulty.SliderMultiplier = 1; editorBeatmap.ControlPointInfo.Clear(); editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length }); + snapProvider.DistanceSpacingMultiplier.Value = 1; Children = new Drawable[] { @@ -64,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor RelativeSizeAxes = Axes.Both, Colour = Color4.SlateGray }, - grid = new TestOsuDistanceSnapGrid(new HitCircle { Position = grid_position }), + grid = new OsuDistanceSnapGrid(new HitCircle { Position = grid_position }), new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position } }; }); @@ -82,25 +101,45 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep($"set beat divisor = {divisor}", () => beatDivisor.Value = divisor); } + [TestCase(1.0f)] + [TestCase(2.0f)] + [TestCase(0.5f)] + public void TestDistanceSpacing(float multiplier) + { + AddStep($"set distance spacing = {multiplier}", () => snapProvider.DistanceSpacingMultiplier.Value = multiplier); + } + [Test] public void TestCursorInCentre() { AddStep("move mouse to centre", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position))); - assertSnappedDistance((float)beat_length); + assertSnappedDistance(0); } [Test] public void TestCursorBeforeMovementPoint() { - AddStep("move mouse to just before movement point", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2((float)beat_length, 0) * 1.49f))); - assertSnappedDistance((float)beat_length); + AddStep("move mouse to just before movement point", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2(beat_length, 0) * 1.45f))); + assertSnappedDistance(beat_length); } [Test] public void TestCursorAfterMovementPoint() { - AddStep("move mouse to just after movement point", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2((float)beat_length, 0) * 1.51f))); - assertSnappedDistance((float)beat_length * 2); + AddStep("move mouse to just after movement point", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2(beat_length, 0) * 1.55f))); + assertSnappedDistance(beat_length * 2); + } + + [TestCase(0.5f, beat_length * 2)] + [TestCase(1, beat_length * 2)] + [TestCase(1.5f, beat_length * 1.5f)] + [TestCase(2f, beat_length * 2)] + public void TestDistanceSpacingAdjust(float multiplier, float expectedDistance) + { + AddStep($"Set distance spacing to {multiplier}", () => snapProvider.DistanceSpacingMultiplier.Value = multiplier); + AddStep("move mouse to point", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2(beat_length, 0) * 2))); + + assertSnappedDistance(expectedDistance); } [Test] @@ -115,13 +154,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor RelativeSizeAxes = Axes.Both, Colour = Color4.SlateGray }, - grid = new TestOsuDistanceSnapGrid(new HitCircle { Position = grid_position }, new HitCircle { StartTime = 200 }), + grid = new OsuDistanceSnapGrid(new HitCircle { Position = grid_position }, new HitCircle { StartTime = 200 }), new SnappingCursorContainer { GetSnapPosition = v => grid.GetSnappedPosition(grid.ToLocalSpace(v)).position } }; }); - AddStep("move mouse outside grid", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2((float)beat_length, 0) * 3f))); - assertSnappedDistance((float)beat_length * 2); + AddStep("move mouse outside grid", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2(beat_length, 0) * 3f))); + assertSnappedDistance(beat_length); } private void assertSnappedDistance(float expectedDistance) => AddAssert($"snap distance = {expectedDistance}", () => @@ -137,6 +176,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private readonly Drawable cursor; + private InputManager inputManager; + + public override bool HandlePositionalInput => true; + public SnappingCursorContainer() { RelativeSizeAxes = Axes.Both; @@ -153,51 +196,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { base.LoadComplete(); - updatePosition(GetContainingInputManager().CurrentState.Mouse.Position); + inputManager = GetContainingInputManager(); } - protected override bool OnMouseMove(MouseMoveEvent e) + protected override void Update() { - base.OnMouseMove(e); - - updatePosition(e.ScreenSpaceMousePosition); - return true; + base.Update(); + cursor.Position = GetSnapPosition.Invoke(inputManager.CurrentState.Mouse.Position); } - - private void updatePosition(Vector2 screenSpacePosition) - { - cursor.Position = GetSnapPosition.Invoke(screenSpacePosition); - } - } - - private class TestOsuDistanceSnapGrid : OsuDistanceSnapGrid - { - public new float DistanceSpacing => base.DistanceSpacing; - - public TestOsuDistanceSnapGrid(OsuHitObject hitObject, OsuHitObject nextHitObject = null) - : base(hitObject, nextHitObject) - { - } - } - - private class SnapProvider : IDistanceSnapProvider - { - public SnapResult FindSnappedPosition(Vector2 screenSpacePosition) => - new SnapResult(screenSpacePosition, null); - - public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0); - - public IBindable DistanceSpacingMultiplier { get; } = new BindableDouble(1); - - public float GetBeatSnapDistanceAt(HitObject referenceObject) => (float)beat_length; - - public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration; - - public double DistanceToDuration(HitObject referenceObject, float distance) => distance; - - public double FindSnappedDuration(HitObject referenceObject, float distance) => 0; - - public float FindSnappedDistance(HitObject referenceObject, float distance) => 0; } } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs index 8a561f962a..b11929c1e8 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Edit public class OsuDistanceSnapGrid : CircularDistanceSnapGrid { public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null) - : base(hitObject, hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime) + : base(hitObject, hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime - 1) { Masking = true; } diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index 51ca55f37f..888002eb36 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -142,13 +142,12 @@ namespace osu.Game.Tests.Visual.Collections AddStep("add dropdown", () => { Add(new CollectionFilterDropdown - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - RelativeSizeAxes = Axes.X, - Width = 0.4f, - } - ); + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.X, + Width = 0.4f, + }); }); AddStep("add two collections with same name", () => manager.Collections.AddRange(new[] { diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index 38c0808a71..3aa3481cbf 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -21,8 +21,12 @@ namespace osu.Game.Tests.Visual.Editing public class TestSceneDistanceSnapGrid : EditorClockTestScene { private const double beat_length = 100; + private const int beat_snap_distance = 10; + private static readonly Vector2 grid_position = new Vector2(512, 384); + private TestDistanceSnapGrid grid; + [Cached(typeof(EditorBeatmap))] private readonly EditorBeatmap editorBeatmap; @@ -39,6 +43,7 @@ namespace osu.Game.Tests.Visual.Editing } }); editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length }); + editorBeatmap.Difficulty.SliderMultiplier = 1; } [SetUp] @@ -51,7 +56,7 @@ namespace osu.Game.Tests.Visual.Editing RelativeSizeAxes = Axes.Both, Colour = Color4.SlateGray }, - new TestDistanceSnapGrid() + grid = new TestDistanceSnapGrid() }; }); @@ -68,9 +73,22 @@ namespace osu.Game.Tests.Visual.Editing AddStep($"set beat divisor = {divisor}", () => BeatDivisor.Value = divisor); } - [Test] - public void TestLimitedDistance() + [TestCase(1.0)] + [TestCase(2.0)] + [TestCase(0.5)] + public void TestDistanceSpacing(double multiplier) { + AddStep($"set distance spacing = {multiplier}", () => snapProvider.DistanceSpacingMultiplier.Value = multiplier); + AddAssert("distance spacing matches multiplier", () => grid.DistanceBetweenTicks == beat_snap_distance * multiplier); + } + + [TestCase(1.0)] + [TestCase(2.0)] + [TestCase(0.5)] + public void TestLimitedDistance(double multiplier) + { + const int end_time = 100; + AddStep("create limited grid", () => { Children = new Drawable[] @@ -80,14 +98,19 @@ namespace osu.Game.Tests.Visual.Editing RelativeSizeAxes = Axes.Both, Colour = Color4.SlateGray }, - new TestDistanceSnapGrid(100) + grid = new TestDistanceSnapGrid(end_time) }; }); + + AddStep($"set distance spacing = {multiplier}", () => snapProvider.DistanceSpacingMultiplier.Value = multiplier); + AddStep("check correct interval count", () => Assert.That((end_time / grid.DistanceBetweenTicks) * multiplier, Is.EqualTo(grid.MaxIntervals))); } private class TestDistanceSnapGrid : DistanceSnapGrid { - public new float DistanceSpacing => base.DistanceSpacing; + public new float DistanceBetweenTicks => base.DistanceBetweenTicks; + + public new int MaxIntervals => base.MaxIntervals; public TestDistanceSnapGrid(double? endTime = null) : base(new HitObject(), grid_position, 0, endTime) @@ -105,7 +128,7 @@ namespace osu.Game.Tests.Visual.Editing int indexFromPlacement = 0; - for (float s = StartPosition.X + DistanceSpacing; s <= DrawWidth && indexFromPlacement < MaxIntervals; s += DistanceSpacing, indexFromPlacement++) + for (float s = StartPosition.X + DistanceBetweenTicks; s <= DrawWidth && indexFromPlacement < MaxIntervals; s += DistanceBetweenTicks, indexFromPlacement++) { AddInternal(new Circle { @@ -118,7 +141,7 @@ namespace osu.Game.Tests.Visual.Editing indexFromPlacement = 0; - for (float s = StartPosition.X - DistanceSpacing; s >= 0 && indexFromPlacement < MaxIntervals; s -= DistanceSpacing, indexFromPlacement++) + for (float s = StartPosition.X - DistanceBetweenTicks; s >= 0 && indexFromPlacement < MaxIntervals; s -= DistanceBetweenTicks, indexFromPlacement++) { AddInternal(new Circle { @@ -131,7 +154,7 @@ namespace osu.Game.Tests.Visual.Editing indexFromPlacement = 0; - for (float s = StartPosition.Y + DistanceSpacing; s <= DrawHeight && indexFromPlacement < MaxIntervals; s += DistanceSpacing, indexFromPlacement++) + for (float s = StartPosition.Y + DistanceBetweenTicks; s <= DrawHeight && indexFromPlacement < MaxIntervals; s += DistanceBetweenTicks, indexFromPlacement++) { AddInternal(new Circle { @@ -144,7 +167,7 @@ namespace osu.Game.Tests.Visual.Editing indexFromPlacement = 0; - for (float s = StartPosition.Y - DistanceSpacing; s >= 0 && indexFromPlacement < MaxIntervals; s -= DistanceSpacing, indexFromPlacement++) + for (float s = StartPosition.Y - DistanceBetweenTicks; s >= 0 && indexFromPlacement < MaxIntervals; s -= DistanceBetweenTicks, indexFromPlacement++) { AddInternal(new Circle { @@ -167,9 +190,11 @@ namespace osu.Game.Tests.Visual.Editing public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0); - public IBindable DistanceSpacingMultiplier { get; } = new BindableDouble(1); + public Bindable DistanceSpacingMultiplier { get; } = new BindableDouble(1); - public float GetBeatSnapDistanceAt(HitObject referenceObject) => 10; + IBindable IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier; + + public float GetBeatSnapDistanceAt(HitObject referenceObject) => beat_snap_distance; public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectScreen.cs index b5f901e51d..4eb14542ba 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectScreen.cs @@ -1,29 +1,37 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Mods; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay; +using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneFreeModSelectScreen : MultiplayerTestScene { + private FreeModSelectScreen freeModSelectScreen; + private readonly Bindable>> availableMods = new Bindable>>(); + + [BackgroundDependencyLoader] + private void load(OsuGameBase osuGameBase) + { + availableMods.BindTo(osuGameBase.AvailableMods); + } + [Test] public void TestFreeModSelect() { - FreeModSelectScreen freeModSelectScreen = null; - - AddStep("create free mod select screen", () => Child = freeModSelectScreen = new FreeModSelectScreen - { - State = { Value = Visibility.Visible } - }); - AddUntilStep("all column content loaded", - () => freeModSelectScreen.ChildrenOfType().Any() - && freeModSelectScreen.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded)); + createFreeModSelect(); AddUntilStep("all visible mods are playable", () => this.ChildrenOfType() @@ -36,5 +44,62 @@ namespace osu.Game.Tests.Visual.Multiplayer freeModSelectScreen.State.Value = visible ? Visibility.Visible : Visibility.Hidden; }); } + + [Test] + public void TestCustomisationNotAvailable() + { + createFreeModSelect(); + + AddStep("select difficulty adjust", () => freeModSelectScreen.SelectedMods.Value = new[] { new OsuModDifficultyAdjust() }); + AddWaitStep("wait some", 3); + AddAssert("customisation area not expanded", () => this.ChildrenOfType().Single().Height == 0); + } + + [Test] + public void TestSelectDeselectAll() + { + createFreeModSelect(); + + AddStep("click select all button", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().ElementAt(1)); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("all mods selected", assertAllAvailableModsSelected); + + AddStep("click deselect all button", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Last()); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("all mods deselected", () => !freeModSelectScreen.SelectedMods.Value.Any()); + } + + private void createFreeModSelect() + { + AddStep("create free mod select screen", () => Child = freeModSelectScreen = new FreeModSelectScreen + { + State = { Value = Visibility.Visible } + }); + AddUntilStep("all column content loaded", + () => freeModSelectScreen.ChildrenOfType().Any() + && freeModSelectScreen.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded)); + } + + private bool assertAllAvailableModsSelected() + { + var allAvailableMods = availableMods.Value + .SelectMany(pair => pair.Value) + .Where(mod => mod.UserPlayable && mod.HasImplementation) + .ToList(); + + foreach (var availableMod in allAvailableMods) + { + if (freeModSelectScreen.SelectedMods.Value.All(selectedMod => selectedMod.GetType() != availableMod.GetType())) + return false; + } + + return true; + } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 8e26d1c562..97cb08e3c1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -7,6 +7,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics.Containers; @@ -17,6 +18,7 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; @@ -56,6 +58,9 @@ namespace osu.Game.Tests.Visual.Multiplayer private TestMultiplayerClient multiplayerClient => multiplayerComponents.MultiplayerClient; private TestMultiplayerRoomManager roomManager => multiplayerComponents.RoomManager; + [Resolved] + private OsuConfigManager config { get; set; } + [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { @@ -668,6 +673,43 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for results", () => multiplayerComponents.CurrentScreen is ResultsScreen); } + [Test] + public void TestGameplayExitFlow() + { + Bindable holdDelay = null; + + AddStep("Set hold delay to zero", () => + { + holdDelay = config.GetBindable(OsuSetting.UIHoldActivationDelay); + holdDelay.Value = 0; + }); + + createRoom(() => new Room + { + Name = { Value = "Test Room" }, + Playlist = + { + new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo) + { + RulesetID = new OsuRuleset().RulesetInfo.OnlineID, + } + } + }); + + enterGameplay(); + + AddUntilStep("wait for playing", () => this.ChildrenOfType().FirstOrDefault()?.LocalUserPlaying.Value == true); + + AddStep("attempt exit without hold", () => InputManager.Key(Key.Escape)); + AddAssert("still in gameplay", () => multiplayerComponents.CurrentScreen is Player); + + AddStep("attempt exit with hold", () => InputManager.PressKey(Key.Escape)); + AddUntilStep("wait for lounge", () => multiplayerComponents.CurrentScreen is Screens.OnlinePlay.Multiplayer.Multiplayer); + + AddStep("stop holding", () => InputManager.ReleaseKey(Key.Escape)); + AddStep("set hold delay to default", () => holdDelay.SetDefault()); + } + [Test] public void TestGameplayDoesntStartWithNonLoadedUser() { diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs index 98574e5d53..7c77ac925e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlayV2.cs @@ -133,22 +133,22 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestChatHeight() { - Bindable configChatHeight = null; + BindableFloat configChatHeight = new BindableFloat(); + config.BindWith(OsuSetting.ChatDisplayHeight, configChatHeight); float newHeight = 0; - AddStep("Bind config chat height", () => configChatHeight = config.GetBindable(OsuSetting.ChatDisplayHeight).GetBoundCopy()); - AddStep("Set config chat height", () => configChatHeight.Value = 0.4f); + AddStep("Reset config chat height", () => configChatHeight.SetDefault()); AddStep("Show overlay", () => chatOverlay.Show()); - AddAssert("Overlay uses config height", () => chatOverlay.Height == 0.4f); - AddStep("Drag overlay to new height", () => + AddAssert("Overlay uses config height", () => chatOverlay.Height == configChatHeight.Default); + AddStep("Click top bar", () => { InputManager.MoveMouseTo(chatOverlayTopBar); InputManager.PressButton(MouseButton.Left); - InputManager.MoveMouseTo(chatOverlayTopBar, new Vector2(0, -300)); - InputManager.ReleaseButton(MouseButton.Left); }); + AddStep("Drag overlay to new height", () => InputManager.MoveMouseTo(chatOverlayTopBar, new Vector2(0, -300))); + AddStep("Stop dragging", () => InputManager.ReleaseButton(MouseButton.Left)); AddStep("Store new height", () => newHeight = chatOverlay.Height); - AddAssert("Config height changed", () => configChatHeight.Value != 0.4f && configChatHeight.Value == newHeight); + AddAssert("Config height changed", () => !configChatHeight.IsDefault && configChatHeight.Value == newHeight); AddStep("Hide overlay", () => chatOverlay.Hide()); AddStep("Show overlay", () => chatOverlay.Show()); AddAssert("Overlay uses new height", () => chatOverlay.Height == newHeight); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs index 288c0cb140..2bb6e58448 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneExpandingContainer.cs @@ -99,15 +99,15 @@ namespace osu.Game.Tests.Visual.UserInterface } /// - /// Tests expanding a container will expand underlying groups if contracted. + /// Tests expanding a container will not expand underlying groups if they were manually contracted by the user. /// [Test] - public void TestExpandingContainerExpandsContractedGroup() + public void TestExpandingContainerDoesNotExpandContractedGroup() { AddStep("contract group", () => toolboxGroup.Expanded.Value = false); AddStep("expand container", () => container.Expanded.Value = true); - AddAssert("group expanded", () => toolboxGroup.Expanded.Value); + AddAssert("group not expanded", () => !toolboxGroup.Expanded.Value); AddAssert("controls expanded", () => slider1.Expanded.Value && slider2.Expanded.Value); AddStep("contract container", () => container.Expanded.Value = false); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs index a3ce4b734b..42ffeba444 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs @@ -18,6 +18,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; +using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface @@ -164,7 +165,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() }); assertCustomisationToggleState(disabled: false, active: true); - AddStep("dismiss mod customisation via mouse", () => + AddStep("dismiss mod customisation via toggle", () => { InputManager.MoveMouseTo(modSelectScreen.ChildrenOfType().Single()); InputManager.Click(MouseButton.Left); @@ -191,6 +192,29 @@ namespace osu.Game.Tests.Visual.UserInterface assertCustomisationToggleState(disabled: true, active: false); // config was dismissed without explicit user action. } + [Test] + public void TestDismissCustomisationViaDimmedArea() + { + createScreen(); + assertCustomisationToggleState(disabled: true, active: false); + + AddStep("select mod requiring configuration", () => SelectedMods.Value = new[] { new OsuModDifficultyAdjust() }); + assertCustomisationToggleState(disabled: false, active: true); + + AddStep("move mouse to settings area", () => InputManager.MoveMouseTo(this.ChildrenOfType().Single())); + AddStep("move mouse to dimmed area", () => + { + InputManager.MoveMouseTo(new Vector2( + modSelectScreen.ScreenSpaceDrawQuad.TopLeft.X, + (modSelectScreen.ScreenSpaceDrawQuad.TopLeft.Y + modSelectScreen.ScreenSpaceDrawQuad.BottomLeft.Y) / 2)); + }); + AddStep("click", () => InputManager.Click(MouseButton.Left)); + assertCustomisationToggleState(disabled: false, active: false); + + AddStep("move mouse to first mod panel", () => InputManager.MoveMouseTo(modSelectScreen.ChildrenOfType().First())); + AddAssert("first mod panel is hovered", () => modSelectScreen.ChildrenOfType().First().IsHovered); + } + /// /// Ensure that two mod overlays are not cross polluting via central settings instances. /// @@ -391,6 +415,42 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("unimplemented mod panel is filtered", () => getPanelForMod(typeof(TestUnimplementedMod)).Filtered.Value); } + [Test] + public void TestDeselectAllViaButton() + { + 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("click deselect all button", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Last()); + InputManager.Click(MouseButton.Left); + }); + AddUntilStep("all mods deselected", () => !SelectedMods.Value.Any()); + } + + [Test] + public void TestCloseViaBackButton() + { + createScreen(); + changeRuleset(0); + + AddStep("select difficulty adjust", () => SelectedMods.Value = new Mod[] { new OsuModDifficultyAdjust() }); + assertCustomisationToggleState(disabled: false, active: true); + AddAssert("back button disabled", () => !this.ChildrenOfType().First().Enabled.Value); + + AddStep("dismiss customisation area", () => InputManager.Key(Key.Escape)); + AddStep("click back button", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + AddAssert("mod select hidden", () => modSelectScreen.State.Value == Visibility.Hidden); + } + private void waitForColumnLoad() => AddUntilStep("all column content loaded", () => modSelectScreen.ChildrenOfType().Any() && modSelectScreen.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded)); diff --git a/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs b/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs index 1b802a0a14..28afd082c3 100644 --- a/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs +++ b/osu.Game/Graphics/Containers/HoldToConfirmContainer.cs @@ -10,12 +10,32 @@ using osu.Game.Configuration; namespace osu.Game.Graphics.Containers { + /// + /// A container which adds a common "hold-to-perform" pattern to a container. + /// + /// + /// This container does not handle triggering the hold/abort operations. + /// To use this class, please call and when necessary. + /// + /// The is exposed as a transforming bindable which smoothly tracks the progress of a hold operation. + /// It can be used for animating and displaying progress directly. + /// public abstract class HoldToConfirmContainer : Container { - public Action Action; + public const double DANGEROUS_HOLD_ACTIVATION_DELAY = 500; private const int fadeout_delay = 200; + /// + /// Whether the associated action is considered dangerous, warranting a longer hold. + /// + public bool IsDangerousAction { get; } + + /// + /// The action to perform when a hold successfully completes. + /// + public Action Action; + /// /// Whether currently in a fired state (and the confirm has been sent). /// @@ -23,46 +43,61 @@ namespace osu.Game.Graphics.Containers private bool confirming; + /// + /// The current activation delay for this control. + /// + public IBindable HoldActivationDelay => holdActivationDelay; + + /// + /// The progress of any ongoing hold operation. 0 means no hold has started; 1 means a hold has been completed. + /// + public IBindable Progress => progress; + /// /// Whether the overlay should be allowed to return from a fired state. /// protected virtual bool AllowMultipleFires => false; - /// - /// Specify a custom activation delay, overriding the game-wide user setting. - /// - /// - /// This should be used in special cases where we want to be extra sure the user knows what they are doing. An example is when changes would be lost. - /// - protected virtual double? HoldActivationDelay => null; + private readonly Bindable progress = new BindableDouble(); - public Bindable Progress = new BindableDouble(); + private readonly Bindable holdActivationDelay = new Bindable(); - private Bindable holdActivationDelay; + [Resolved] + private OsuConfigManager config { get; set; } - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + protected HoldToConfirmContainer(bool isDangerousAction = false) { - holdActivationDelay = HoldActivationDelay != null - ? new Bindable(HoldActivationDelay.Value) - : config.GetBindable(OsuSetting.UIHoldActivationDelay); + IsDangerousAction = isDangerousAction; } + protected override void LoadComplete() + { + base.LoadComplete(); + + if (IsDangerousAction) + holdActivationDelay.Value = DANGEROUS_HOLD_ACTIVATION_DELAY; + else + config.BindWith(OsuSetting.UIHoldActivationDelay, holdActivationDelay); + } + + /// + /// Begin a new confirmation. Should be called when the container is interacted with (ie. the user presses a key). + /// + /// + /// Calling this method when already in the process of confirming has no effect. + /// protected void BeginConfirm() { if (confirming || (!AllowMultipleFires && Fired)) return; confirming = true; - this.TransformBindableTo(Progress, 1, holdActivationDelay.Value * (1 - Progress.Value), Easing.Out).OnComplete(_ => Confirm()); - } - - protected virtual void Confirm() - { - Action?.Invoke(); - Fired = true; + this.TransformBindableTo(progress, 1, holdActivationDelay.Value * (1 - progress.Value), Easing.Out).OnComplete(_ => Confirm()); } + /// + /// Abort any ongoing confirmation. Should be called when the container's interaction is no longer valid (ie. the user releases a key). + /// protected void AbortConfirm() { if (!AllowMultipleFires && Fired) return; @@ -71,9 +106,19 @@ namespace osu.Game.Graphics.Containers Fired = false; this - .TransformBindableTo(Progress, Progress.Value) + .TransformBindableTo(progress, progress.Value) .Delay(200) - .TransformBindableTo(Progress, 0, fadeout_delay, Easing.InSine); + .TransformBindableTo(progress, 0, fadeout_delay, Easing.InSine); + } + + /// + /// A method which is invoked when the confirmation sequence completes successfully. + /// By default, will fire the associated . + /// + protected virtual void Confirm() + { + Action?.Invoke(); + Fired = true; } } } diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index 52e9811cf7..9cd626af0f 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -59,6 +59,16 @@ namespace osu.Game.Localisation /// public static LocalisableString Importing => new TranslatableString(getKey(@"importing"), @"Importing..."); + /// + /// "Deselect All" + /// + public static LocalisableString DeselectAll => new TranslatableString(getKey(@"deselect_all"), @"Deselect All"); + + /// + /// "Select All" + /// + public static LocalisableString SelectAll => new TranslatableString(getKey(@"select_all"), @"Select All"); + private static string getKey(string key) => $@"{prefix}:{key}"; } } diff --git a/osu.Game/Localisation/DifficultyMultiplierDisplayStrings.cs b/osu.Game/Localisation/DifficultyMultiplierDisplayStrings.cs new file mode 100644 index 0000000000..c281d90190 --- /dev/null +++ b/osu.Game/Localisation/DifficultyMultiplierDisplayStrings.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class DifficultyMultiplierDisplayStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.DifficultyMultiplierDisplay"; + + /// + /// "Difficulty Multiplier" + /// + public static LocalisableString DifficultyMultiplier => new TranslatableString(getKey(@"difficulty_multiplier"), @"Difficulty Multiplier"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} \ No newline at end of file diff --git a/osu.Game/Localisation/ModSelectScreenStrings.cs b/osu.Game/Localisation/ModSelectScreenStrings.cs new file mode 100644 index 0000000000..0c113fd381 --- /dev/null +++ b/osu.Game/Localisation/ModSelectScreenStrings.cs @@ -0,0 +1,29 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Localisation; + +namespace osu.Game.Localisation +{ + public static class ModSelectScreenStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.ModSelectScreen"; + + /// + /// "Mod Select" + /// + public static LocalisableString ModSelectTitle => new TranslatableString(getKey(@"mod_select_title"), @"Mod Select"); + + /// + /// "Mods provide different ways to enjoy gameplay. Some have an effect on the score you can achieve during ranked play. Others are just for fun." + /// + public static LocalisableString ModSelectDescription => new TranslatableString(getKey(@"mod_select_description"), @"Mods provide different ways to enjoy gameplay. Some have an effect on the score you can achieve during ranked play. Others are just for fun."); + + /// + /// "Mod Customisation" + /// + public static LocalisableString ModCustomisation => new TranslatableString(getKey(@"mod_customisation"), @"Mod Customisation"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} \ No newline at end of file diff --git a/osu.Game/Migrations/20171019041408_InitialCreate.cs b/osu.Game/Migrations/20171019041408_InitialCreate.cs index 9b6881f98c..08ab64fd08 100644 --- a/osu.Game/Migrations/20171019041408_InitialCreate.cs +++ b/osu.Game/Migrations/20171019041408_InitialCreate.cs @@ -1,4 +1,7 @@ -using Microsoft.EntityFrameworkCore.Migrations; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations { diff --git a/osu.Game/Migrations/20171025071459_AddMissingIndexRules.cs b/osu.Game/Migrations/20171025071459_AddMissingIndexRules.cs index c9fc59c5a2..4ec3952941 100644 --- a/osu.Game/Migrations/20171025071459_AddMissingIndexRules.cs +++ b/osu.Game/Migrations/20171025071459_AddMissingIndexRules.cs @@ -1,4 +1,7 @@ -using Microsoft.EntityFrameworkCore.Migrations; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations { diff --git a/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.cs b/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.cs index 084ae67940..6aba12f86f 100644 --- a/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.cs +++ b/osu.Game/Migrations/20171119065731_AddBeatmapOnlineIDUniqueConstraint.cs @@ -1,4 +1,7 @@ -using Microsoft.EntityFrameworkCore.Migrations; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations { diff --git a/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.cs b/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.cs index 09cf0af89c..5688455f79 100644 --- a/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.cs +++ b/osu.Game/Migrations/20171209034410_AddRulesetInfoShortName.cs @@ -1,4 +1,7 @@ -using Microsoft.EntityFrameworkCore.Migrations; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations { diff --git a/osu.Game/Migrations/20180125143340_Settings.cs b/osu.Game/Migrations/20180125143340_Settings.cs index 166d3c086d..1feb37531f 100644 --- a/osu.Game/Migrations/20180125143340_Settings.cs +++ b/osu.Game/Migrations/20180125143340_Settings.cs @@ -1,4 +1,7 @@ -using Microsoft.EntityFrameworkCore.Migrations; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations { diff --git a/osu.Game/Migrations/20180131154205_AddMuteBinding.cs b/osu.Game/Migrations/20180131154205_AddMuteBinding.cs index 5564a30bbf..8646d1d76b 100644 --- a/osu.Game/Migrations/20180131154205_AddMuteBinding.cs +++ b/osu.Game/Migrations/20180131154205_AddMuteBinding.cs @@ -1,4 +1,7 @@ -using Microsoft.EntityFrameworkCore.Migrations; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Infrastructure; using osu.Game.Database; using osu.Game.Input.Bindings; diff --git a/osu.Game/Migrations/20180219060912_AddSkins.cs b/osu.Game/Migrations/20180219060912_AddSkins.cs index a0270ab0fd..319748bed6 100644 --- a/osu.Game/Migrations/20180219060912_AddSkins.cs +++ b/osu.Game/Migrations/20180219060912_AddSkins.cs @@ -1,4 +1,7 @@ -using Microsoft.EntityFrameworkCore.Migrations; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations { diff --git a/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.cs b/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.cs index 27269cc5fc..91eabe8868 100644 --- a/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.cs +++ b/osu.Game/Migrations/20180529055154_RemoveUniqueHashConstraints.cs @@ -1,4 +1,7 @@ -using Microsoft.EntityFrameworkCore.Migrations; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations { diff --git a/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.cs b/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.cs index 71304ea979..d888ccd5a2 100644 --- a/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.cs +++ b/osu.Game/Migrations/20180621044111_UpdateTaikoDefaultBindings.cs @@ -1,4 +1,7 @@ -using Microsoft.EntityFrameworkCore.Migrations; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations { diff --git a/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs b/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs index 506d65f761..fdea636ac6 100644 --- a/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs +++ b/osu.Game/Migrations/20180628011956_RemoveNegativeSetIDs.cs @@ -1,4 +1,7 @@ -using Microsoft.EntityFrameworkCore.Migrations; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations { diff --git a/osu.Game/Migrations/20180913080842_AddRankStatus.cs b/osu.Game/Migrations/20180913080842_AddRankStatus.cs index bba4944bb7..bb147dff84 100644 --- a/osu.Game/Migrations/20180913080842_AddRankStatus.cs +++ b/osu.Game/Migrations/20180913080842_AddRankStatus.cs @@ -1,4 +1,7 @@ -using Microsoft.EntityFrameworkCore.Migrations; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations { diff --git a/osu.Game/Migrations/20181007180454_StandardizePaths.cs b/osu.Game/Migrations/20181007180454_StandardizePaths.cs index 274b8030a9..30f27043a0 100644 --- a/osu.Game/Migrations/20181007180454_StandardizePaths.cs +++ b/osu.Game/Migrations/20181007180454_StandardizePaths.cs @@ -1,6 +1,7 @@ -using System; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using Microsoft.EntityFrameworkCore.Migrations; -using System.IO; namespace osu.Game.Migrations { diff --git a/osu.Game/Migrations/20181128100659_AddSkinInfoHash.cs b/osu.Game/Migrations/20181128100659_AddSkinInfoHash.cs index 860264a7dd..ee825a1e9c 100644 --- a/osu.Game/Migrations/20181128100659_AddSkinInfoHash.cs +++ b/osu.Game/Migrations/20181128100659_AddSkinInfoHash.cs @@ -1,4 +1,7 @@ -using Microsoft.EntityFrameworkCore.Migrations; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations { diff --git a/osu.Game/Migrations/20181130113755_AddScoreInfoTables.cs b/osu.Game/Migrations/20181130113755_AddScoreInfoTables.cs index 2b6f94c5a4..58980132f3 100644 --- a/osu.Game/Migrations/20181130113755_AddScoreInfoTables.cs +++ b/osu.Game/Migrations/20181130113755_AddScoreInfoTables.cs @@ -1,4 +1,7 @@ -using System; +// 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 Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations diff --git a/osu.Game/Migrations/20190225062029_AddUserIDColumn.cs b/osu.Game/Migrations/20190225062029_AddUserIDColumn.cs index 0720e0eac7..f2eef600dc 100644 --- a/osu.Game/Migrations/20190225062029_AddUserIDColumn.cs +++ b/osu.Game/Migrations/20190225062029_AddUserIDColumn.cs @@ -1,4 +1,7 @@ -using Microsoft.EntityFrameworkCore.Migrations; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations { diff --git a/osu.Game/Migrations/20190525060824_SkinSettings.cs b/osu.Game/Migrations/20190525060824_SkinSettings.cs index 99237419b7..7779b55bb7 100644 --- a/osu.Game/Migrations/20190525060824_SkinSettings.cs +++ b/osu.Game/Migrations/20190525060824_SkinSettings.cs @@ -1,4 +1,7 @@ -using Microsoft.EntityFrameworkCore.Migrations; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations { diff --git a/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.cs b/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.cs index 55dc18b6a3..0620a0624f 100644 --- a/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.cs +++ b/osu.Game/Migrations/20190605091246_AddDateAddedColumnToBeatmapSet.cs @@ -1,4 +1,7 @@ -using System; +// 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 Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations diff --git a/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.cs b/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.cs index f5963ebf5e..f8ce354aa1 100644 --- a/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.cs +++ b/osu.Game/Migrations/20190708070844_AddBPMAndLengthColumns.cs @@ -1,4 +1,7 @@ -using Microsoft.EntityFrameworkCore.Migrations; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations { diff --git a/osu.Game/Migrations/20190913104727_AddBeatmapVideo.cs b/osu.Game/Migrations/20190913104727_AddBeatmapVideo.cs index 9ed0943acd..af82b4db20 100644 --- a/osu.Game/Migrations/20190913104727_AddBeatmapVideo.cs +++ b/osu.Game/Migrations/20190913104727_AddBeatmapVideo.cs @@ -1,4 +1,7 @@ -using Microsoft.EntityFrameworkCore.Migrations; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations { diff --git a/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.cs b/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.cs index ec4475971c..3d2ddbf6fc 100644 --- a/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.cs +++ b/osu.Game/Migrations/20200302094919_RefreshVolumeBindings.cs @@ -1,4 +1,7 @@ -using Microsoft.EntityFrameworkCore.Migrations; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations { diff --git a/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.cs b/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.cs index be6968aa5d..58a35a7bf3 100644 --- a/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.cs +++ b/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.cs @@ -1,4 +1,7 @@ -using Microsoft.EntityFrameworkCore.Migrations; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations { diff --git a/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.cs b/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.cs index 155d6670a8..4d3941dd20 100644 --- a/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.cs +++ b/osu.Game/Migrations/20210412045700_RefreshVolumeBindingsAgain.cs @@ -1,4 +1,7 @@ -using Microsoft.EntityFrameworkCore.Migrations; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations { diff --git a/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.cs b/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.cs index 1d5b0769a4..887635fa85 100644 --- a/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.cs +++ b/osu.Game/Migrations/20210511060743_AddSkinInstantiationInfo.cs @@ -1,4 +1,7 @@ -using Microsoft.EntityFrameworkCore.Migrations; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations { diff --git a/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.cs b/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.cs index 98fe9b5e13..7b579e27b9 100644 --- a/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.cs +++ b/osu.Game/Migrations/20210514062639_AddAuthorIdToBeatmapMetadata.cs @@ -1,4 +1,7 @@ -using Microsoft.EntityFrameworkCore.Migrations; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations { diff --git a/osu.Game/Migrations/20210824185035_AddCountdownSettings.cs b/osu.Game/Migrations/20210824185035_AddCountdownSettings.cs index 564f5f4520..d1b09e2c1d 100644 --- a/osu.Game/Migrations/20210824185035_AddCountdownSettings.cs +++ b/osu.Game/Migrations/20210824185035_AddCountdownSettings.cs @@ -1,4 +1,7 @@ -using Microsoft.EntityFrameworkCore.Migrations; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations { diff --git a/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.cs b/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.cs index bf3f855d5f..f6fc1f4420 100644 --- a/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.cs +++ b/osu.Game/Migrations/20210912144011_AddSamplesMatchPlaybackRate.cs @@ -1,4 +1,7 @@ -using Microsoft.EntityFrameworkCore.Migrations; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Microsoft.EntityFrameworkCore.Migrations; namespace osu.Game.Migrations { diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 8c9741b98b..62ddd49881 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -458,7 +458,7 @@ namespace osu.Game.Online.API public GuestUser() { Username = @"Guest"; - Id = 1; + Id = SYSTEM_USER_ID; } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index fcf5889a7e..c2a96a9082 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1182,6 +1182,7 @@ namespace osu.Game horizontalOffset += (Content.ToLocalSpace(Notifications.ScreenSpaceDrawQuad.TopLeft).X - Content.DrawWidth) * SIDE_OVERLAY_OFFSET_RATIO; ScreenOffsetContainer.X = horizontalOffset; + overlayContent.X = horizontalOffset * 1.2f; MenuCursorContainer.CanShowCursor = (ScreenStack.CurrentScreen as IOsuScreen)?.CursorVisible ?? false; } diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index 9ee002fd9d..f528f54cdf 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -119,7 +119,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores maxComboColumn.Text = value.MaxCombo.ToLocalisableString(@"0\x"); ppColumn.Alpha = value.BeatmapInfo.Status.GrantsPerformancePoints() ? 1 : 0; - ppColumn.Text = value.PP?.ToLocalisableString(@"N0") ?? default(LocalisableString); + ppColumn.Text = value.PP?.ToLocalisableString(@"N0") ?? default; statisticsColumns.ChildrenEnumerable = value.GetStatisticsForDisplay().Select(createStatisticsColumn); modsColumn.Mods = value.Mods; diff --git a/osu.Game/Overlays/ChatOverlayV2.cs b/osu.Game/Overlays/ChatOverlayV2.cs index cab88136fc..4c7fa0f802 100644 --- a/osu.Game/Overlays/ChatOverlayV2.cs +++ b/osu.Game/Overlays/ChatOverlayV2.cs @@ -39,7 +39,7 @@ namespace osu.Game.Overlays private ChatTextBar textBar = null!; private Container currentChannelContainer = null!; - private readonly Bindable chatHeight = new Bindable(); + private readonly BindableFloat chatHeight = new BindableFloat(); private bool isDraggingTopBar; private float dragStartChatHeight; diff --git a/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs b/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs index adc627e15b..6239c5e409 100644 --- a/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs +++ b/osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs @@ -42,7 +42,10 @@ namespace osu.Game.Overlays.Dialog private class DangerousConfirmContainer : HoldToConfirmContainer { - protected override double? HoldActivationDelay => 500; + public DangerousConfirmContainer() + : base(isDangerousAction: true) + { + } protected override bool OnMouseDown(MouseDownEvent e) { diff --git a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs index 1d848fe456..4ccec0dd87 100644 --- a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs +++ b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs @@ -15,6 +15,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Mods; using osuTK; +using osu.Game.Localisation; namespace osu.Game.Overlays.Mods { @@ -99,7 +100,7 @@ namespace osu.Game.Overlays.Mods Origin = Anchor.Centre, Margin = new MarginPadding { Horizontal = 18 }, Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0), - Text = "Difficulty Multiplier", + Text = DifficultyMultiplierDisplayStrings.DifficultyMultiplier, Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold) } } diff --git a/osu.Game/Overlays/Mods/ModColumn.cs b/osu.Game/Overlays/Mods/ModColumn.cs index a792c0a81e..cbd036c71f 100644 --- a/osu.Game/Overlays/Mods/ModColumn.cs +++ b/osu.Game/Overlays/Mods/ModColumn.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -22,6 +23,7 @@ using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Localisation; using osu.Game.Rulesets.Mods; using osu.Game.Utils; using osuTK; @@ -49,7 +51,7 @@ namespace osu.Game.Overlays.Mods set { filter = value; - updateFilter(); + updateState(); } } @@ -220,7 +222,6 @@ namespace osu.Game.Overlays.Mods Origin = Anchor.CentreLeft, Scale = new Vector2(0.8f), RelativeSizeAxes = Axes.X, - LabelText = "Enable All", Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0) }); panelFlow.Padding = new MarginPadding @@ -265,6 +266,19 @@ namespace osu.Game.Overlays.Mods contentBackground.Colour = colourProvider.Background4; } + protected override void LoadComplete() + { + base.LoadComplete(); + + toggleAllCheckbox?.Current.BindValueChanged(_ => updateToggleAllText(), true); + } + + private void updateToggleAllText() + { + Debug.Assert(toggleAllCheckbox != null); + toggleAllCheckbox.LabelText = toggleAllCheckbox.Current.Value ? CommonStrings.DeselectAll : CommonStrings.SelectAll; + } + private void updateLocalAvailableMods() { var newMods = ModUtils.FlattenMods(availableMods.Value.GetValueOrDefault(ModType) ?? Array.Empty()) @@ -275,32 +289,25 @@ namespace osu.Game.Overlays.Mods return; localAvailableMods = newMods; - Scheduler.AddOnce(loadPanels); + + if (!IsLoaded) + // if we're coming from BDL, perform the first load synchronously to make sure everything is in place as early as possible. + onPanelsLoaded(createPanels()); + else + asyncLoadPanels(); } private CancellationTokenSource? cancellationTokenSource; - private void loadPanels() + private void asyncLoadPanels() { cancellationTokenSource?.Cancel(); - var panels = localAvailableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0))); + var panels = createPanels(); Task? loadTask; - latestLoadTask = loadTask = LoadComponentsAsync(panels, loaded => - { - panelFlow.ChildrenEnumerable = loaded; - - updateActiveState(); - updateToggleAllState(); - updateFilter(); - - foreach (var panel in panelFlow) - { - panel.Active.BindValueChanged(_ => panelStateChanged(panel)); - } - }, (cancellationTokenSource = new CancellationTokenSource()).Token); + latestLoadTask = loadTask = LoadComponentsAsync(panels, onPanelsLoaded, (cancellationTokenSource = new CancellationTokenSource()).Token); loadTask.ContinueWith(_ => { if (loadTask == latestLoadTask) @@ -308,10 +315,37 @@ namespace osu.Game.Overlays.Mods }); } - private void updateActiveState() + private IEnumerable createPanels() + { + var panels = localAvailableMods.Select(mod => CreateModPanel(mod).With(panel => panel.Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0))); + return panels; + } + + private void onPanelsLoaded(IEnumerable loaded) + { + panelFlow.ChildrenEnumerable = loaded; + + updateState(); + + foreach (var panel in panelFlow) + { + panel.Active.BindValueChanged(_ => panelStateChanged(panel)); + } + } + + private void updateState() { foreach (var panel in panelFlow) + { panel.Active.Value = SelectedMods.Contains(panel.Mod); + panel.ApplyFilter(Filter); + } + + if (toggleAllCheckbox != null && !SelectionAnimationRunning) + { + toggleAllCheckbox.Alpha = panelFlow.Any(panel => !panel.Filtered.Value) ? 1 : 0; + toggleAllCheckbox.Current.Value = panelFlow.Where(panel => !panel.Filtered.Value).All(panel => panel.Active.Value); + } } /// @@ -323,15 +357,16 @@ namespace osu.Game.Overlays.Mods private void panelStateChanged(ModPanel panel) { - updateToggleAllState(); + if (externalSelectionUpdateInProgress) + return; var newSelectedMods = panel.Active.Value ? SelectedMods.Append(panel.Mod) : SelectedMods.Except(panel.Mod.Yield()); SelectedMods = newSelectedMods.ToArray(); - if (!externalSelectionUpdateInProgress) - SelectionChangedByUser?.Invoke(); + updateState(); + SelectionChangedByUser?.Invoke(); } /// @@ -364,7 +399,7 @@ namespace osu.Game.Overlays.Mods } SelectedMods = newSelection; - updateActiveState(); + updateState(); externalSelectionUpdateInProgress = false; } @@ -378,7 +413,7 @@ namespace osu.Game.Overlays.Mods private readonly Queue pendingSelectionOperations = new Queue(); - protected bool SelectionAnimationRunning => pendingSelectionOperations.Count > 0; + internal bool SelectionAnimationRunning => pendingSelectionOperations.Count > 0; protected override void Update() { @@ -403,15 +438,6 @@ namespace osu.Game.Overlays.Mods } } - private void updateToggleAllState() - { - if (toggleAllCheckbox != null && !SelectionAnimationRunning) - { - toggleAllCheckbox.Alpha = panelFlow.Any(panel => !panel.Filtered.Value) ? 1 : 0; - toggleAllCheckbox.Current.Value = panelFlow.Where(panel => !panel.Filtered.Value).All(panel => panel.Active.Value); - } - } - /// /// Selects all mods. /// @@ -507,18 +533,6 @@ namespace osu.Game.Overlays.Mods #endregion - #region Filtering support - - private void updateFilter() - { - foreach (var modPanel in panelFlow) - modPanel.ApplyFilter(Filter); - - updateToggleAllState(); - } - - #endregion - #region Keyboard selection support protected override bool OnKeyDown(KeyDownEvent e) diff --git a/osu.Game/Overlays/Mods/ModSelectScreen.cs b/osu.Game/Overlays/Mods/ModSelectScreen.cs index 7c0ed941c6..7d0400aace 100644 --- a/osu.Game/Overlays/Mods/ModSelectScreen.cs +++ b/osu.Game/Overlays/Mods/ModSelectScreen.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Framework.Layout; +using osu.Framework.Lists; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics; @@ -23,6 +24,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select; using osuTK; using osuTK.Input; +using osu.Game.Localisation; namespace osu.Game.Overlays.Mods { @@ -54,11 +56,6 @@ namespace osu.Game.Overlays.Mods /// internal GlobalAction? Hotkey { get; set; } - /// - /// Whether configurable s can be configured by the local user. - /// - protected virtual bool AllowCustomisation => true; - /// /// Whether the total score multiplier calculated from the current selected set of mods should be shown. /// @@ -66,12 +63,28 @@ namespace osu.Game.Overlays.Mods protected virtual ModColumn CreateModColumn(ModType modType, Key[]? toggleKeys = null) => new ModColumn(modType, false, toggleKeys); + protected virtual IEnumerable CreateFooterButtons() => new[] + { + customisationButton = new ShearedToggleButton(200) + { + Text = ModSelectScreenStrings.ModCustomisation, + Active = { BindTarget = customisationVisible } + }, + new ShearedButton(200) + { + Text = CommonStrings.DeselectAll, + Action = DeselectAll + } + }; + private readonly BindableBool customisationVisible = new BindableBool(); private DifficultyMultiplierDisplay? multiplierDisplay; private ModSettingsArea modSettingsArea = null!; private ColumnScrollContainer columnScroll = null!; private ColumnFlowContainer columnFlow = null!; + private ShearedToggleButton? customisationButton; + private FillFlowContainer footerButtonFlow = null!; protected ModSelectScreen(OverlayColourScheme colourScheme = OverlayColourScheme.Green) : base(colourScheme) @@ -79,10 +92,10 @@ namespace osu.Game.Overlays.Mods } [BackgroundDependencyLoader] - private void load() + private void load(OsuColour colours) { - Header.Title = "Mod Select"; - Header.Description = "Mods provide different ways to enjoy gameplay. Some have an effect on the score you can achieve during ranked play. Others are just for fun."; + Header.Title = ModSelectScreenStrings.ModSelectTitle; + Header.Description = ModSelectScreenStrings.ModSelectDescription; AddRange(new Drawable[] { @@ -107,6 +120,7 @@ namespace osu.Game.Overlays.Mods Padding = new MarginPadding { Top = (ShowTotalMultiplier ? DifficultyMultiplierDisplay.HEIGHT : 0) + PADDING, + Bottom = PADDING }, RelativeSizeAxes = Axes.Both, RelativePositionAxes = Axes.Both, @@ -157,17 +171,27 @@ namespace osu.Game.Overlays.Mods }); } - if (AllowCustomisation) + FooterContent.Child = footerButtonFlow = new FillFlowContainer { - Footer.Add(new ShearedToggleButton(200) + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Padding = new MarginPadding { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Margin = new MarginPadding { Vertical = PADDING, Left = 70 }, - Text = "Mod Customisation", - Active = { BindTarget = customisationVisible } - }); - } + Vertical = PADDING, + Horizontal = 70 + }, + Spacing = new Vector2(10), + ChildrenEnumerable = CreateFooterButtons().Prepend(new ShearedButton(200) + { + Text = CommonStrings.Back, + Action = Hide, + DarkerColour = colours.Pink2, + LighterColour = colours.Pink1 + }) + }; } private ColumnDimContainer createModColumnContent(ModType modType, Key[]? toggleKeys = null) @@ -222,7 +246,7 @@ namespace osu.Game.Overlays.Mods private void updateCustomisation(ValueChangedEvent> valueChangedEvent) { - if (!AllowCustomisation) + if (customisationButton == null) return; bool anyCustomisableMod = false; @@ -256,6 +280,12 @@ namespace osu.Game.Overlays.Mods MainAreaContent.FadeColour(customisationVisible.Value ? Colour4.Gray : Colour4.White, transition_duration, Easing.InOutCubic); + foreach (var button in footerButtonFlow) + { + if (button != customisationButton) + button.Enabled.Value = !customisationVisible.Value; + } + float modAreaHeight = customisationVisible.Value ? ModSettingsArea.HEIGHT : 0; modSettingsArea.ResizeHeightTo(modAreaHeight, transition_duration, Easing.InOutCubic); @@ -277,7 +307,9 @@ namespace osu.Game.Overlays.Mods { var candidateSelection = columnFlow.Columns.SelectMany(column => column.SelectedMods).ToArray(); - if (candidateSelection.SequenceEqual(SelectedMods.Value)) + // the following guard intends to check cases where we've already replaced potentially-external mod references with our own and avoid endless recursion. + // TODO: replace custom comparer with System.Collections.Generic.ReferenceEqualityComparer when fully on .NET 6 + if (candidateSelection.SequenceEqual(SelectedMods.Value, new FuncEqualityComparer(ReferenceEquals))) return; SelectedMods.Value = ComputeNewModsFromSelection(SelectedMods.Value, candidateSelection); @@ -329,6 +361,18 @@ namespace osu.Game.Overlays.Mods } } + protected void SelectAll() + { + foreach (var column in columnFlow.Columns) + column.SelectAll(); + } + + protected void DeselectAll() + { + foreach (var column in columnFlow.Columns) + column.DeselectAll(); + } + public override bool OnPressed(KeyBindingPressEvent e) { if (e.Repeat) @@ -448,6 +492,8 @@ namespace osu.Game.Overlays.Mods FinishTransforms(); } + protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate || Column.SelectionAnimationRunning; + private void updateDim() { Colour4 targetColour; @@ -488,6 +534,8 @@ namespace osu.Game.Overlays.Mods public Action? OnClicked { get; set; } + public override bool HandlePositionalInput => base.HandlePositionalInput && HandleMouse.Value; + protected override bool Handle(UIEvent e) { if (!HandleMouse.Value) diff --git a/osu.Game/Overlays/Mods/ModSettingsArea.cs b/osu.Game/Overlays/Mods/ModSettingsArea.cs index 9c5f3b7f11..08f563f93e 100644 --- a/osu.Game/Overlays/Mods/ModSettingsArea.cs +++ b/osu.Game/Overlays/Mods/ModSettingsArea.cs @@ -44,7 +44,6 @@ namespace osu.Game.Overlays.Mods { RelativeSizeAxes = Axes.Both, Masking = true, - BorderThickness = 2, Children = new Drawable[] { background = new Box diff --git a/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs b/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs index ec9cb55042..d8eb5b65ac 100644 --- a/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs +++ b/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs @@ -62,7 +62,7 @@ namespace osu.Game.Overlays.Profile.Header.Components private void updateProgress(APIUser user) { levelProgressBar.Length = user?.Statistics?.Level.Progress / 100f ?? 0; - levelProgressText.Text = user?.Statistics?.Level.Progress.ToLocalisableString("0'%'") ?? default(LocalisableString); + levelProgressText.Text = user?.Statistics?.Level.Progress.ToLocalisableString("0'%'") ?? default; } } } diff --git a/osu.Game/Overlays/Rankings/SpotlightSelector.cs b/osu.Game/Overlays/Rankings/SpotlightSelector.cs index 48a4c31f30..c05c160463 100644 --- a/osu.Game/Overlays/Rankings/SpotlightSelector.cs +++ b/osu.Game/Overlays/Rankings/SpotlightSelector.cs @@ -126,7 +126,7 @@ namespace osu.Game.Overlays.Rankings startDateColumn.Value = dateToString(response.Spotlight.StartDate); endDateColumn.Value = dateToString(response.Spotlight.EndDate); mapCountColumn.Value = response.BeatmapSets.Count.ToLocalisableString(@"N0"); - participantsColumn.Value = response.Spotlight.Participants?.ToLocalisableString(@"N0") ?? default(LocalisableString); + participantsColumn.Value = response.Spotlight.Participants?.ToLocalisableString(@"N0") ?? default; } private LocalisableString dateToString(DateTimeOffset date) => date.ToLocalisableString(@"yyyy-MM-dd"); diff --git a/osu.Game/Overlays/Rankings/Tables/PerformanceTable.cs b/osu.Game/Overlays/Rankings/Tables/PerformanceTable.cs index bdbd2942d1..6c85ec2753 100644 --- a/osu.Game/Overlays/Rankings/Tables/PerformanceTable.cs +++ b/osu.Game/Overlays/Rankings/Tables/PerformanceTable.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Localisation; using osu.Game.Resources.Localisation.Web; using osu.Game.Users; @@ -25,7 +24,7 @@ namespace osu.Game.Overlays.Rankings.Tables protected override Drawable[] CreateUniqueContent(UserStatistics item) => new Drawable[] { - new RowText { Text = item.PP?.ToLocalisableString(@"N0") ?? default(LocalisableString), } + new RowText { Text = item.PP?.ToLocalisableString(@"N0") ?? default, } }; } } diff --git a/osu.Game/Overlays/SettingsToolboxGroup.cs b/osu.Game/Overlays/SettingsToolboxGroup.cs index b9e5283a44..808d4fc422 100644 --- a/osu.Game/Overlays/SettingsToolboxGroup.cs +++ b/osu.Game/Overlays/SettingsToolboxGroup.cs @@ -1,7 +1,6 @@ // 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.Caching; using osu.Framework.Extensions.EnumExtensions; @@ -35,14 +34,13 @@ namespace osu.Game.Overlays private readonly Cached headerTextVisibilityCache = new Cached(); private readonly FillFlowContainer content; - private readonly IconButton button; public BindableBool Expanded { get; } = new BindableBool(true); - private Color4 expandedColour; - private readonly OsuSpriteText headerText; + private readonly Container headerContent; + /// /// Create a new instance. /// @@ -71,7 +69,7 @@ namespace osu.Game.Overlays AutoSizeAxes = Axes.Y, Children = new Drawable[] { - new Container + headerContent = new Container { Name = @"Header", Origin = Anchor.TopCentre, @@ -88,7 +86,7 @@ namespace osu.Game.Overlays Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 17), Padding = new MarginPadding { Left = 10, Right = 30 }, }, - button = new IconButton + new IconButton { Origin = Anchor.Centre, Anchor = Anchor.CentreRight, @@ -117,12 +115,25 @@ namespace osu.Game.Overlays }; } - protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) + protected override void LoadComplete() { - if (invalidation.HasFlagFast(Invalidation.DrawSize)) - headerTextVisibilityCache.Invalidate(); + base.LoadComplete(); - return base.OnInvalidate(invalidation, source); + Expanded.BindValueChanged(updateExpandedState, true); + + this.Delay(600).Schedule(updateFadeState); + } + + protected override bool OnHover(HoverEvent e) + { + updateFadeState(); + return false; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateFadeState(); + base.OnHoverLost(e); } protected override void Update() @@ -135,72 +146,30 @@ namespace osu.Game.Overlays headerText.FadeTo(headerText.DrawWidth < DrawWidth ? 1 : 0, 150, Easing.OutQuint); } - [Resolved(canBeNull: true)] - private IExpandingContainer expandingContainer { get; set; } - - private bool expandedByContainer; - - protected override void LoadComplete() + protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) { - base.LoadComplete(); + if (invalidation.HasFlagFast(Invalidation.DrawSize)) + headerTextVisibilityCache.Invalidate(); - expandingContainer?.Expanded.BindValueChanged(containerExpanded => + return base.OnInvalidate(invalidation, source); + } + + private void updateExpandedState(ValueChangedEvent expanded) + { + if (expanded.NewValue) + content.AutoSizeAxes = Axes.Y; + else { - if (containerExpanded.NewValue && !Expanded.Value) - { - Expanded.Value = true; - expandedByContainer = true; - } - else if (!containerExpanded.NewValue && expandedByContainer) - { - Expanded.Value = false; - expandedByContainer = false; - } + content.AutoSizeAxes = Axes.None; + content.ResizeHeightTo(0, transition_duration, Easing.OutQuint); + } - updateActiveState(); - }, true); - - Expanded.BindValueChanged(v => - { - // clearing transforms can break autosizing, see: https://github.com/ppy/osu-framework/issues/5064 - if (v.NewValue != v.OldValue) - content.ClearTransforms(); - - if (v.NewValue) - content.AutoSizeAxes = Axes.Y; - else - { - content.AutoSizeAxes = Axes.None; - content.ResizeHeightTo(0, transition_duration, Easing.OutQuint); - } - - button.FadeColour(Expanded.Value ? expandedColour : Color4.White, 200, Easing.InOutQuint); - }, true); - - this.Delay(600).Schedule(updateActiveState); + headerContent.FadeColour(expanded.NewValue ? Color4.White : OsuColour.Gray(0.5f), 200, Easing.OutQuint); } - protected override bool OnHover(HoverEvent e) + private void updateFadeState() { - updateActiveState(); - return false; - } - - protected override void OnHoverLost(HoverLostEvent e) - { - updateActiveState(); - base.OnHoverLost(e); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - expandedColour = colours.Yellow; - } - - private void updateActiveState() - { - this.FadeTo(IsHovered || expandingContainer?.Expanded.Value == true ? 1 : inactive_alpha, fade_duration, Easing.OutQuint); + this.FadeTo(IsHovered ? 1 : inactive_alpha, fade_duration, Easing.OutQuint); } protected override Container Content => content; diff --git a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs index bbcb702bd8..5e6d9dbe34 100644 --- a/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/DistancedHitObjectComposer.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Edit public abstract class DistancedHitObjectComposer : HitObjectComposer, IDistanceSnapProvider, IScrollBindingHandler where TObject : HitObject { - protected Bindable DistanceSpacingMultiplier { get; } = new BindableDouble(1.0) + public Bindable DistanceSpacingMultiplier { get; } = new BindableDouble(1.0) { MinValue = 0.1, MaxValue = 6.0, diff --git a/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs index e807dbd482..c6cc09a16c 100644 --- a/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs +++ b/osu.Game/Rulesets/Edit/ExpandingToolboxContainer.cs @@ -1,7 +1,6 @@ // 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; @@ -25,7 +24,7 @@ namespace osu.Game.Rulesets.Edit public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) && anyToolboxHovered(screenSpacePos); - private bool anyToolboxHovered(Vector2 screenSpacePos) => FillFlow.Children.Any(d => d.ScreenSpaceDrawQuad.Contains(screenSpacePos)); + private bool anyToolboxHovered(Vector2 screenSpacePos) => FillFlow.ScreenSpaceDrawQuad.Contains(screenSpacePos); protected override bool OnMouseDown(MouseDownEvent e) => true; diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs index 8c599f8596..b12e1437dc 100644 --- a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs @@ -16,6 +16,7 @@ namespace osu.Game.Rulesets.Edit { /// /// A multiplier which changes the ratio of distance travelled per time unit. + /// Importantly, this is provided for manual usage, and not multiplied into any of the methods exposed by this interface. /// /// IBindable DistanceSpacingMultiplier { get; } diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs index 50d5f0389a..91fad08aff 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs @@ -30,14 +30,14 @@ namespace osu.Game.Screens.Edit.Compose.Components Position = StartPosition, Width = crosshair_thickness, EdgeSmoothness = new Vector2(1), - Height = Math.Min(crosshair_max_size, DistanceSpacing * 2), + Height = Math.Min(crosshair_max_size, DistanceBetweenTicks * 2), }, new Box { Origin = Anchor.Centre, Position = StartPosition, EdgeSmoothness = new Vector2(1), - Width = Math.Min(crosshair_max_size, DistanceSpacing * 2), + Width = Math.Min(crosshair_max_size, DistanceBetweenTicks * 2), Height = crosshair_thickness, } }); @@ -45,19 +45,19 @@ namespace osu.Game.Screens.Edit.Compose.Components float dx = Math.Max(StartPosition.X, DrawWidth - StartPosition.X); float dy = Math.Max(StartPosition.Y, DrawHeight - StartPosition.Y); float maxDistance = new Vector2(dx, dy).Length; - int requiredCircles = Math.Min(MaxIntervals, (int)(maxDistance / DistanceSpacing)); + int requiredCircles = Math.Min(MaxIntervals, (int)(maxDistance / DistanceBetweenTicks)); for (int i = 0; i < requiredCircles; i++) { - float radius = (i + 1) * DistanceSpacing * 2; + float diameter = (i + 1) * DistanceBetweenTicks * 2; AddInternal(new CircularProgress { Origin = Anchor.Centre, Position = StartPosition, Current = { Value = 1 }, - Size = new Vector2(radius), - InnerRadius = 4 * 1f / radius, + Size = new Vector2(diameter), + InnerRadius = 4 * 1f / diameter, Colour = GetColourForIndexFromPlacement(i) }); } @@ -68,19 +68,37 @@ namespace osu.Game.Screens.Edit.Compose.Components if (MaxIntervals == 0) return (StartPosition, StartTime); - Vector2 direction = position - StartPosition; - if (direction == Vector2.Zero) - direction = new Vector2(0.001f, 0.001f); + // This grid implementation factors in the user's distance spacing specification, + // which is usually not considered by an `IDistanceSnapProvider`. + float distanceSpacingMultiplier = (float)DistanceSpacingMultiplier.Value; - float distance = direction.Length; + Vector2 travelVector = (position - StartPosition); - float radius = DistanceSpacing; - int radialCount = Math.Clamp((int)MathF.Round(distance / radius), 1, MaxIntervals); + if (travelVector == Vector2.Zero) + return (StartPosition, StartTime); - Vector2 normalisedDirection = direction * new Vector2(1f / distance); - Vector2 snappedPosition = StartPosition + normalisedDirection * radialCount * radius; + float travelLength = travelVector.Length; - return (snappedPosition, StartTime + SnapProvider.FindSnappedDuration(ReferenceObject, (snappedPosition - StartPosition).Length)); + // FindSnappedDistance will always round down, but we want to potentially round upwards. + travelLength += DistanceBetweenTicks / 2; + + // When interacting with the resolved snap provider, the distance spacing multiplier should first be removed + // to allow for snapping at a non-multiplied ratio. + float snappedDistance = SnapProvider.FindSnappedDistance(ReferenceObject, travelLength / distanceSpacingMultiplier); + double snappedTime = StartTime + SnapProvider.DistanceToDuration(ReferenceObject, snappedDistance); + + if (snappedTime > LatestEndTime) + { + double tickLength = Beatmap.GetBeatLengthAtTime(StartTime); + + snappedDistance = SnapProvider.DurationToDistance(ReferenceObject, MaxIntervals * tickLength); + snappedTime = StartTime + SnapProvider.DistanceToDuration(ReferenceObject, snappedDistance); + } + + // The multiplier can then be reapplied to the final position. + Vector2 snappedPosition = StartPosition + travelVector.Normalized() * snappedDistance * distanceSpacingMultiplier; + + return (snappedPosition, snappedTime); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs index 5568c15514..2f39db06d4 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs @@ -23,7 +23,9 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// The spacing between each tick of the beat snapping grid. /// - protected float DistanceSpacing { get; private set; } + protected float DistanceBetweenTicks { get; private set; } + + protected IBindable DistanceSpacingMultiplier { get; private set; } /// /// The maximum number of distance snapping intervals allowed. @@ -32,7 +34,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// The position which the grid should start. - /// The first beat snapping tick is located at + away from this point. + /// The first beat snapping tick is located at + away from this point. /// protected readonly Vector2 StartPosition; @@ -41,6 +43,8 @@ namespace osu.Game.Screens.Edit.Compose.Components /// protected readonly double StartTime; + protected readonly double? LatestEndTime; + [Resolved] protected OsuColour Colours { get; private set; } @@ -48,15 +52,12 @@ namespace osu.Game.Screens.Edit.Compose.Components protected IDistanceSnapProvider SnapProvider { get; private set; } [Resolved] - private EditorBeatmap beatmap { get; set; } + protected EditorBeatmap Beatmap { get; private set; } [Resolved] private BindableBeatDivisor beatDivisor { get; set; } - private IBindable distanceSpacingMultiplier; - private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); - private readonly double? endTime; protected readonly HitObject ReferenceObject; @@ -70,7 +71,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected DistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null) { ReferenceObject = referenceObject; - this.endTime = endTime; + LatestEndTime = endTime; StartPosition = startPosition; StartTime = startTime; @@ -86,22 +87,21 @@ namespace osu.Game.Screens.Edit.Compose.Components beatDivisor.BindValueChanged(_ => updateSpacing()); - distanceSpacingMultiplier = SnapProvider.DistanceSpacingMultiplier.GetBoundCopy(); - distanceSpacingMultiplier.BindValueChanged(_ => updateSpacing(), true); + DistanceSpacingMultiplier = SnapProvider.DistanceSpacingMultiplier.GetBoundCopy(); + DistanceSpacingMultiplier.BindValueChanged(_ => updateSpacing(), true); } private void updateSpacing() { - DistanceSpacing = (float)(SnapProvider.GetBeatSnapDistanceAt(ReferenceObject) * distanceSpacingMultiplier.Value); + float distanceSpacingMultiplier = (float)DistanceSpacingMultiplier.Value; + float beatSnapDistance = SnapProvider.GetBeatSnapDistanceAt(ReferenceObject); - if (endTime == null) + DistanceBetweenTicks = beatSnapDistance * distanceSpacingMultiplier; + + if (LatestEndTime == null) MaxIntervals = int.MaxValue; else - { - // +1 is added since a snapped hitobject may have its start time slightly less than the snapped time due to floating point errors - double maxDuration = endTime.Value - StartTime + 1; - MaxIntervals = (int)(maxDuration / SnapProvider.DistanceToDuration(ReferenceObject, DistanceSpacing)); - } + MaxIntervals = (int)((LatestEndTime.Value - StartTime) / SnapProvider.DistanceToDuration(ReferenceObject, beatSnapDistance)); gridCache.Invalidate(); } @@ -137,7 +137,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The applicable colour. protected ColourInfo GetColourForIndexFromPlacement(int placementIndex) { - var timingPoint = beatmap.ControlPointInfo.TimingPointAt(StartTime); + var timingPoint = Beatmap.ControlPointInfo.TimingPointAt(StartTime); double beatLength = timingPoint.BeatLength / beatDivisor.Value; int beatIndex = (int)Math.Round((StartTime - timingPoint.Time) / beatLength); diff --git a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs index eaee2cd1e2..ecbac82db0 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs @@ -9,7 +9,8 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; -using osuTK.Graphics; +using osu.Framework.Layout; +using osuTK; namespace osu.Game.Screens.Edit.Compose.Components { @@ -41,17 +42,7 @@ namespace osu.Game.Screens.Edit.Compose.Components InternalChild = Box = CreateBox(); } - protected virtual Drawable CreateBox() => new Container - { - Masking = true, - BorderColour = Color4.White, - BorderThickness = SelectionBox.BORDER_RADIUS, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0.1f - } - }; + protected virtual Drawable CreateBox() => new BoxWithBorders(); private RectangleF? dragRectangle; @@ -111,5 +102,75 @@ namespace osu.Game.Screens.Edit.Compose.Components public override void Show() => State = Visibility.Visible; public event Action StateChanged; + + public class BoxWithBorders : CompositeDrawable + { + private readonly LayoutValue cache = new LayoutValue(Invalidation.RequiredParentSizeToFit); + + public BoxWithBorders() + { + AddLayout(cache); + } + + protected override void Update() + { + base.Update(); + + if (!cache.IsValid) + { + createContent(); + cache.Validate(); + } + } + + private void createContent() + { + if (DrawSize == Vector2.Zero) + { + ClearInternal(); + return; + } + + // Make lines the same width independent of display resolution. + float lineThickness = DrawWidth > 0 + ? DrawWidth / ScreenSpaceDrawQuad.Width * 2 + : DrawHeight / ScreenSpaceDrawQuad.Height * 2; + + Padding = new MarginPadding(-lineThickness / 2); + + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.X, + Height = lineThickness, + }, + new Box + { + RelativeSizeAxes = Axes.X, + Height = lineThickness, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + }, + new Box + { + RelativeSizeAxes = Axes.Y, + Width = lineThickness, + }, + new Box + { + RelativeSizeAxes = Axes.Y, + Width = lineThickness, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.1f + } + }; + } + } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs index 22479bd9b3..f13ed0456a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs @@ -100,7 +100,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void updateTooltipText() { - TooltipText = cumulativeRotation.Value?.ToLocalisableString("0.0°") ?? default(LocalisableString); + TooltipText = cumulativeRotation.Value?.ToLocalisableString("0.0°") ?? default; } } } diff --git a/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs b/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs index 613f16563c..f360a80599 100644 --- a/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs +++ b/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs @@ -87,44 +87,46 @@ namespace osu.Game.Screens.OnlinePlay.Components }, }; - // case MatchType.TagCoop: - // return new SpriteIcon - // { - // Anchor = Anchor.Centre, - // Origin = Anchor.Centre, - // Size = new Vector2(size), - // Icon = FontAwesome.Solid.Sync, - // Colour = colours.Blue, - // - // Shadow = false - // }; +#pragma warning disable IDE0055 // Indentation of commented code + // case MatchType.TagCoop: + // return new SpriteIcon + // { + // Anchor = Anchor.Centre, + // Origin = Anchor.Centre, + // Size = new Vector2(size), + // Icon = FontAwesome.Solid.Sync, + // Colour = colours.Blue, + // + // Shadow = false + // }; - // case MatchType.TagTeamCoop: - // return new FillFlowContainer - // { - // Anchor = Anchor.Centre, - // Origin = Anchor.Centre, - // AutoSizeAxes = Axes.Both, - // Direction = FillDirection.Horizontal, - // Spacing = new Vector2(2f), - // Children = new[] - // { - // new SpriteIcon - // { - // Icon = FontAwesome.Solid.Sync, - // Size = new Vector2(size * 0.75f), - // Colour = colours.Blue, - // Shadow = false, - // }, - // new SpriteIcon - // { - // Icon = FontAwesome.Solid.Sync, - // Size = new Vector2(size * 0.75f), - // Colour = colours.Pink, - // Shadow = false, - // }, - // }, - // }; + // case MatchType.TagTeamCoop: + // return new FillFlowContainer + // { + // Anchor = Anchor.Centre, + // Origin = Anchor.Centre, + // AutoSizeAxes = Axes.Both, + // Direction = FillDirection.Horizontal, + // Spacing = new Vector2(2f), + // Children = new[] + // { + // new SpriteIcon + // { + // Icon = FontAwesome.Solid.Sync, + // Size = new Vector2(size * 0.75f), + // Colour = colours.Blue, + // Shadow = false, + // }, + // new SpriteIcon + // { + // Icon = FontAwesome.Solid.Sync, + // Size = new Vector2(size * 0.75f), + // Colour = colours.Pink, + // Shadow = false, + // }, + // }, + // }; +#pragma warning restore IDE0055 } } diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs index 63b7d1dbb5..b6b40ca5a6 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs @@ -3,15 +3,18 @@ using System; using osu.Game.Overlays; +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Mods; using osuTK.Input; +using osu.Game.Localisation; namespace osu.Game.Screens.OnlinePlay { public class FreeModSelectScreen : ModSelectScreen { - protected override bool AllowCustomisation => false; protected override bool ShowTotalMultiplier => false; public new Func IsValidMod @@ -27,5 +30,23 @@ namespace osu.Game.Screens.OnlinePlay } protected override ModColumn CreateModColumn(ModType modType, Key[] toggleKeys = null) => new ModColumn(modType, true, toggleKeys); + + protected override IEnumerable CreateFooterButtons() => new[] + { + new ShearedButton(200) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Text = CommonStrings.SelectAll, + Action = SelectAll + }, + new ShearedButton(200) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Text = CommonStrings.DeselectAll, + Action = DeselectAll + } + }; } } diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index 3da63ec2cc..2ba76d0896 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -12,13 +12,14 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Threading; using osu.Framework.Utils; -using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Input.Bindings; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { @@ -28,20 +29,22 @@ namespace osu.Game.Screens.Play.HUD public readonly Bindable IsPaused = new Bindable(); - private readonly Button button; + private HoldButton button; - public Action Action - { - set => button.Action = value; - } + public Action Action { get; set; } - private readonly OsuSpriteText text; + private OsuSpriteText text; public HoldForMenuButton() { Direction = FillDirection.Horizontal; Spacing = new Vector2(20, 0); Margin = new MarginPadding(10); + } + + [BackgroundDependencyLoader(true)] + private void load(Player player) + { Children = new Drawable[] { text = new OsuSpriteText @@ -50,25 +53,20 @@ namespace osu.Game.Screens.Play.HUD Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft }, - button = new Button + button = new HoldButton(player?.Configuration.AllowRestart == false) { HoverGained = () => text.FadeIn(500, Easing.OutQuint), HoverLost = () => text.FadeOut(500, Easing.OutQuint), - IsPaused = { BindTarget = IsPaused } + IsPaused = { BindTarget = IsPaused }, + Action = () => Action(), } }; AutoSizeAxes = Axes.Both; } - [Resolved] - private OsuConfigManager config { get; set; } - - private Bindable activationDelay; - protected override void LoadComplete() { - activationDelay = config.GetBindable(OsuSetting.UIHoldActivationDelay); - activationDelay.BindValueChanged(v => + button.HoldActivationDelay.BindValueChanged(v => { text.Text = v.NewValue > 0 ? "hold for menu" @@ -102,7 +100,7 @@ namespace osu.Game.Screens.Play.HUD } } - private class Button : HoldToConfirmContainer, IKeyBindingHandler + private class HoldButton : HoldToConfirmContainer, IKeyBindingHandler { private SpriteIcon icon; private CircularProgress circularProgress; @@ -115,6 +113,16 @@ namespace osu.Game.Screens.Play.HUD public Action HoverGained; public Action HoverLost; + private const double shake_duration = 20; + + private bool pendingAnimation; + private ScheduledDelegate shakeOperation; + + public HoldButton(bool isDangerousAction) + : base(isDangerousAction) + { + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -161,11 +169,38 @@ namespace osu.Game.Screens.Play.HUD private void bind() { - circularProgress.Current.BindTo(Progress); - Progress.ValueChanged += progress => icon.Scale = new Vector2(1 + (float)progress.NewValue * 0.2f); + ((IBindable)circularProgress.Current).BindTo(Progress); + Progress.ValueChanged += progress => + { + icon.Scale = new Vector2(1 + (float)progress.NewValue * 0.2f); + + if (IsDangerousAction) + { + Colour = Interpolation.ValueAt(progress.NewValue, Color4.White, Color4.Red, 0, 1, Easing.OutQuint); + + if (progress.NewValue > 0 && progress.NewValue < 1) + { + shakeOperation ??= Scheduler.AddDelayed(shake, shake_duration, true); + } + else + { + Child.MoveTo(Vector2.Zero, shake_duration * 2, Easing.OutQuint); + shakeOperation?.Cancel(); + shakeOperation = null; + } + } + }; } - private bool pendingAnimation; + private void shake() + { + const float shake_magnitude = 8; + + Child.MoveTo(new Vector2( + RNG.NextSingle(-1, 1) * (float)Progress.Value * shake_magnitude, + RNG.NextSingle(-1, 1) * (float)Progress.Value * shake_magnitude + ), shake_duration); + } protected override void Confirm() { diff --git a/osu.iOS.props b/osu.iOS.props index e472b5f1a8..f987ae9bf8 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -1,4 +1,4 @@ - + 8.0 {FEACFBD2-3405-455C-9665-78FE426C6842};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} diff --git a/osu.sln b/osu.sln index b5018db362..aeec0843be 100644 --- a/osu.sln +++ b/osu.sln @@ -56,6 +56,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{10DF8F12-50FD-45D8-8A38-17BA764BF54D}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + .globalconfig = .globalconfig Directory.Build.props = Directory.Build.props osu.Android.props = osu.Android.props osu.iOS.props = osu.iOS.props