diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 5a3eadf607fb..1132396608f0 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 840fa9833441..c0ea55f4c879 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 f2066f27de96..514acef52554 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 000000000000..462dbc74ed8e --- /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 709545bf1d4b..73a150d3e375 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 b6260fd1d461..97d9dbc3806d 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -1,4 +1,4 @@ - + 8.0 bin\$(Configuration) @@ -52,7 +52,7 @@ - + diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs index e345e03c9614..88fd3b36ba79 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs @@ -29,13 +29,14 @@ public abstract class CatchSelectionBlueprintTestScene : SelectionBlueprintTestS 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.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs index cca3701a6077..fec253924fc3 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs @@ -55,7 +55,10 @@ public void TestReversePlacement() AddMoveStep(end_time, 0); AddClickStep(MouseButton.Left); + AddMoveStep(start_time, 0); + AddAssert("duration is positive", () => ((BananaShower)CurrentBlueprint.HitObject).Duration > 0); + AddClickStep(MouseButton.Right); AddAssert("start time is correct", () => Precision.AlmostEquals(LastObject.HitObject.StartTime, start_time)); AddAssert("end time is correct", () => Precision.AlmostEquals(LastObject.HitObject.GetEndTime(), end_time)); diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs index 6dea8b071279..039008f901ec 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/BananaShowerPlacementBlueprint.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Input.Events; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; using osu.Game.Rulesets.Catch.Objects; @@ -13,11 +14,21 @@ public class BananaShowerPlacementBlueprint : CatchPlacementBlueprint 0); return true; } @@ -61,13 +65,16 @@ public override void UpdateTimeAndPosition(SnapResult result) switch (PlacementActive) { case PlacementState.Waiting: - HitObject.StartTime = time; + placementStartTime = placementEndTime = time; break; case PlacementState.Active: - HitObject.EndTime = time; + placementEndTime = time; break; } + + HitObject.StartTime = Math.Min(placementStartTime, placementEndTime); + HitObject.EndTime = Math.Max(placementStartTime, placementEndTime); } } } diff --git a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs index cff5bc2417c1..e9c8e2bb2cec 100644 --- a/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch/Edit/Blueprints/JuiceStreamPlacementBlueprint.cs @@ -47,6 +47,8 @@ protected override void LoadComplete() base.LoadComplete(); inputManager = GetContainingInputManager(); + + BeginPlacement(); } protected override bool OnMouseDown(MouseDownEvent e) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs index 22ace52c2a04..0b34ab28a3d9 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs @@ -107,6 +107,18 @@ protected override void CreateNestedHitObjects(CancellationToken cancellationTok { switch (e.Type) { + case SliderEventType.Tick: + AddNested(new SliderTick + { + SpanIndex = e.SpanIndex, + SpanStartTime = e.SpanStartTime, + StartTime = e.Time, + Position = Position + Path.PositionAt(e.PathProgress), + StackHeight = StackHeight, + Scale = Scale, + }); + break; + case SliderEventType.Head: AddNested(HeadCircle = new SliderHeadCircle { diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index 4c126f0a3bba..6c9dddf51fa9 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -137,33 +137,137 @@ public void TestNotAllowedThroughBaseType() // incompatible pair. new object[] { - new Mod[] { new OsuModDoubleTime(), new OsuModHalfTime() }, - new[] { typeof(OsuModDoubleTime), typeof(OsuModHalfTime) } + new Mod[] { new OsuModHidden(), new OsuModApproachDifferent() }, + new[] { typeof(OsuModHidden), typeof(OsuModApproachDifferent) } }, // incompatible pair with derived class. new object[] { - new Mod[] { new OsuModNightcore(), new OsuModHalfTime() }, - new[] { typeof(OsuModNightcore), typeof(OsuModHalfTime) } + new Mod[] { new OsuModDeflate(), new OsuModApproachDifferent() }, + new[] { typeof(OsuModDeflate), typeof(OsuModApproachDifferent) } }, // system mod. new object[] { - new Mod[] { new OsuModDoubleTime(), new OsuModTouchDevice() }, + new Mod[] { new OsuModHidden(), new OsuModTouchDevice() }, new[] { typeof(OsuModTouchDevice) } }, // multi mod. new object[] { - new Mod[] { new MultiMod(new OsuModHalfTime()), new OsuModDaycore() }, + new Mod[] { new MultiMod(new OsuModSuddenDeath(), new OsuModPerfect()) }, new[] { typeof(MultiMod) } }, + // invalid multiplayer mod is valid for local. + new object[] + { + new Mod[] { new OsuModHidden(), new InvalidMultiplayerMod() }, + null + }, + // invalid free mod is valid for local. + new object[] + { + new Mod[] { new OsuModHidden(), new InvalidMultiplayerFreeMod() }, + null + }, + // valid pair. + new object[] + { + new Mod[] { new OsuModHidden(), new OsuModHardRock() }, + null + }, + }; + + private static readonly object[] invalid_multiplayer_mod_test_scenarios = + { + // incompatible pair. + new object[] + { + new Mod[] { new OsuModHidden(), new OsuModApproachDifferent() }, + new[] { typeof(OsuModHidden), typeof(OsuModApproachDifferent) } + }, + // incompatible pair with derived class. + new object[] + { + new Mod[] { new OsuModDeflate(), new OsuModApproachDifferent() }, + new[] { typeof(OsuModDeflate), typeof(OsuModApproachDifferent) } + }, + // system mod. + new object[] + { + new Mod[] { new OsuModHidden(), new OsuModTouchDevice() }, + new[] { typeof(OsuModTouchDevice) } + }, + // multi mod. + new object[] + { + new Mod[] { new MultiMod(new OsuModSuddenDeath(), new OsuModPerfect()) }, + new[] { typeof(MultiMod) } + }, + // invalid multiplayer mod. + new object[] + { + new Mod[] { new OsuModHidden(), new InvalidMultiplayerMod() }, + new[] { typeof(InvalidMultiplayerMod) } + }, + // invalid free mod is valid for multiplayer global. + new object[] + { + new Mod[] { new OsuModHidden(), new InvalidMultiplayerFreeMod() }, + null + }, + // valid pair. + new object[] + { + new Mod[] { new OsuModHidden(), new OsuModHardRock() }, + null + }, + }; + + private static readonly object[] invalid_free_mod_test_scenarios = + { + // system mod. + new object[] + { + new Mod[] { new OsuModHidden(), new OsuModTouchDevice() }, + new[] { typeof(OsuModTouchDevice) } + }, + // multi mod. + new object[] + { + new Mod[] { new MultiMod(new OsuModSuddenDeath(), new OsuModPerfect()) }, + new[] { typeof(MultiMod) } + }, + // invalid multiplayer mod. + new object[] + { + new Mod[] { new OsuModHidden(), new InvalidMultiplayerMod() }, + new[] { typeof(InvalidMultiplayerMod) } + }, + // invalid free mod. + new object[] + { + new Mod[] { new OsuModHidden(), new InvalidMultiplayerFreeMod() }, + new[] { typeof(InvalidMultiplayerFreeMod) } + }, + // incompatible pair is valid for free mods. + new object[] + { + new Mod[] { new OsuModHidden(), new OsuModApproachDifferent() }, + null, + }, + // incompatible pair with derived class is valid for free mods. + new object[] + { + new Mod[] { new OsuModDeflate(), new OsuModSpinIn() }, + null, + }, // valid pair. new object[] { - new Mod[] { new OsuModDoubleTime(), new OsuModHardRock() }, + new Mod[] { new OsuModHidden(), new OsuModHardRock() }, null - } + }, }; [TestCaseSource(nameof(invalid_mod_test_scenarios))] @@ -179,6 +283,32 @@ public void TestInvalidModScenarios(Mod[] inputMods, Type[] expectedInvalid) Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid)); } + [TestCaseSource(nameof(invalid_multiplayer_mod_test_scenarios))] + public void TestInvalidMultiplayerModScenarios(Mod[] inputMods, Type[] expectedInvalid) + { + bool isValid = ModUtils.CheckValidRequiredModsForMultiplayer(inputMods, out var invalid); + + Assert.That(isValid, Is.EqualTo(expectedInvalid == null)); + + if (isValid) + Assert.IsNull(invalid); + else + Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid)); + } + + [TestCaseSource(nameof(invalid_free_mod_test_scenarios))] + public void TestInvalidFreeModScenarios(Mod[] inputMods, Type[] expectedInvalid) + { + bool isValid = ModUtils.CheckValidFreeModsForMultiplayer(inputMods, out var invalid); + + Assert.That(isValid, Is.EqualTo(expectedInvalid == null)); + + if (isValid) + Assert.IsNull(invalid); + else + Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid)); + } + public abstract class CustomMod1 : Mod, IModCompatibilitySpecification { } @@ -187,6 +317,27 @@ public abstract class CustomMod2 : Mod, IModCompatibilitySpecification { } + public class InvalidMultiplayerMod : Mod + { + public override string Name => string.Empty; + public override string Description => string.Empty; + public override string Acronym => string.Empty; + public override double ScoreMultiplier => 1; + public override bool HasImplementation => true; + public override bool ValidForMultiplayer => false; + public override bool ValidForMultiplayerAsFreeMod => false; + } + + private class InvalidMultiplayerFreeMod : Mod + { + public override string Name => string.Empty; + public override string Description => string.Empty; + public override string Acronym => string.Empty; + public override double ScoreMultiplier => 1; + public override bool HasImplementation => true; + public override bool ValidForMultiplayerAsFreeMod => false; + } + public interface IModCompatibilitySpecification { } diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index f7140537ee76..ef115ab66bb2 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.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 System.Threading; using NUnit.Framework; using osu.Framework.Allocation; @@ -283,7 +282,7 @@ private void setupUserSettings() AddUntilStep("Song select has selection", () => songSelect.Carousel?.SelectedBeatmapInfo != null); AddStep("Set default user settings", () => { - SelectedMods.Value = SelectedMods.Value.Concat(new[] { new OsuModNoFail() }).ToArray(); + SelectedMods.Value = new[] { new OsuModNoFail() }; songSelect.DimLevel.Value = 0.7f; songSelect.BlurLevel.Value = 0.4f; }); diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index 51ca55f37f14..888002eb3699 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -142,13 +142,12 @@ public void TestCollectionNameCollisions() 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/Gameplay/TestSceneGameplaySamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs index ae2bc60fc60b..815cc0944803 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs @@ -5,6 +5,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; +using osu.Game.Audio; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects.Drawables; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs index 64afe1235b2e..31abcb674885 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs @@ -15,7 +15,6 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Testing; using osu.Game.Audio; -using osu.Game.Screens.Play; using osu.Game.Skinning; namespace osu.Game.Tests.Visual.Gameplay @@ -131,7 +130,6 @@ public void TestSkinChangeDoesntPlayOnPause() } [Cached(typeof(ISkinSource))] - [Cached(typeof(ISamplePlaybackDisabler))] private class TestSkinSourceContainer : Container, ISkinSource, ISamplePlaybackDisabler { [Resolved] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectScreen.cs index b5f901e51d30..4eb14542ba39 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 @@ public void TestFreeModSelect() 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 8e45d99eae2e..97cb08e3c16e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -627,7 +627,7 @@ public void TestLeaveNavigation() AddStep("invoke on back button", () => multiplayerComponents.OnBackButton()); - AddAssert("mod overlay is hidden", () => this.ChildrenOfType().Single().State.Value == Visibility.Hidden); + AddAssert("mod overlay is hidden", () => this.ChildrenOfType().Single().State.Value == Visibility.Hidden); AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 714951cc4285..061fe5715b5b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -132,7 +132,11 @@ public void TestAllowedModDeselectedWhenRequired(Type allowedMod, Type requiredM private void assertHasFreeModButton(Type type, bool hasButton = true) { AddAssert($"{type.ReadableName()} {(hasButton ? "displayed" : "not displayed")} in freemod overlay", - () => songSelect.ChildrenOfType().Single().ChildrenOfType().All(b => b.Mod.GetType() != type)); + () => this.ChildrenOfType() + .Single() + .ChildrenOfType() + .Where(panel => !panel.Filtered.Value) + .All(b => b.Mod.GetType() != type)); } private class TestMultiplayerMatchSongSelect : MultiplayerMatchSongSelect diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 057032c41359..2abde82e9279 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -168,8 +168,13 @@ public void TestFreeModSelectionHasAllowedMods() ClickButtonWhenEnabled(); + AddUntilStep("mod select contents loaded", + () => this.ChildrenOfType().Any() && this.ChildrenOfType().All(col => col.IsLoaded && col.ItemsLoaded)); AddUntilStep("mod select contains only double time mod", - () => this.ChildrenOfType().SingleOrDefault()?.ChildrenOfType().SingleOrDefault()?.Mod is OsuModDoubleTime); + () => this.ChildrenOfType() + .SingleOrDefault()? + .ChildrenOfType() + .SingleOrDefault(panel => !panel.Filtered.Value)?.Mod is OsuModDoubleTime); } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs index bcb36a585f1f..0237298fa1fb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs @@ -8,11 +8,23 @@ using osu.Game.Rulesets.Osu; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Tests.Resources; +using osuTK; namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMultiplayerTeamResults : ScreenTestScene { + [Test] + public void TestScaling() + { + // scheduling is needed as scaling the content immediately causes the entire scene to shake badly, for some odd reason. + AddSliderStep("scale", 0.5f, 1.6f, 1f, v => Schedule(() => + { + Stack.Scale = new Vector2(v); + Stack.Size = new Vector2(1f / v); + })); + } + [TestCase(7483253, 1048576)] [TestCase(1048576, 7483253)] [TestCase(1048576, 1048576)] diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index a1f41d4caf69..9674ef7ae131 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -11,6 +11,7 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Leaderboards; using osu.Game.Overlays; @@ -253,12 +254,12 @@ public void TestExitSongSelectWithClick() PushAndConfirm(() => songSelect = new TestPlaySongSelect()); AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show()); AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible); - AddStep("Move mouse to backButton", () => InputManager.MoveMouseTo(backButtonPosition)); - // BackButton handles hover using its child button, so this checks whether or not any of BackButton's children are hovered. - AddUntilStep("Back button is hovered", () => Game.ChildrenOfType().First().Children.Any(c => c.IsHovered)); + AddStep("Move mouse to dimmed area", () => InputManager.MoveMouseTo(new Vector2( + songSelect.ScreenSpaceDrawQuad.TopLeft.X + 1, + songSelect.ScreenSpaceDrawQuad.TopLeft.Y + songSelect.ScreenSpaceDrawQuad.Height / 2))); + AddStep("Click left mouse button", () => InputManager.Click(MouseButton.Left)); - AddStep("Click back button", () => InputManager.Click(MouseButton.Left)); AddUntilStep("Overlay was hidden", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden); exitViaBackButtonAndConfirm(); } @@ -503,6 +504,22 @@ public void TestExitGameFromSongSelect() AddStep("test dispose doesn't crash", () => Game.Dispose()); } + [Test] + public void TestRapidBackButtonExit() + { + AddStep("set hold delay to 0", () => Game.LocalConfig.SetValue(OsuSetting.UIHoldActivationDelay, 0.0)); + + AddStep("press escape twice rapidly", () => + { + InputManager.Key(Key.Escape); + InputManager.Key(Key.Escape); + }); + + pushEscape(); + + AddAssert("exit dialog is shown", () => Game.Dependencies.Get().CurrentDialog != null); + } + private Func playToResults() { Player player = null; @@ -551,7 +568,7 @@ private void exitViaBackButtonAndConfirm() public class TestPlaySongSelect : PlaySongSelect { - public ModSelectOverlay ModSelectOverlay => ModSelect; + public ModSelectScreen ModSelectOverlay => ModSelect; public BeatmapOptionsOverlay BeatmapOptionsOverlay => BeatmapOptions; diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorSceneLibrary.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs similarity index 80% rename from osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorSceneLibrary.cs rename to osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs index d3aeba2c0f0f..68c44f49cc03 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorSceneLibrary.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -4,6 +4,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Extensions; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Framework.Testing; using osu.Game.Overlays.Settings; @@ -18,22 +19,23 @@ namespace osu.Game.Tests.Visual.Navigation { - public class TestSceneSkinEditorSceneLibrary : OsuGameTestScene + public class TestSceneSkinEditorNavigation : OsuGameTestScene { - private SkinEditor skinEditor; + private TestPlaySongSelect songSelect; + private SkinEditor skinEditor => Game.ChildrenOfType().FirstOrDefault(); - public override void SetUpSteps() + private void advanceToSongSelect() { - base.SetUpSteps(); - - Screens.Select.SongSelect songSelect = null; PushAndConfirm(() => songSelect = new TestPlaySongSelect()); AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded); AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + } + private void openSkinEditor() + { AddStep("open skin editor", () => { InputManager.PressKey(Key.ControlLeft); @@ -42,13 +44,15 @@ public override void SetUpSteps() InputManager.ReleaseKey(Key.ControlLeft); InputManager.ReleaseKey(Key.ShiftLeft); }); - - AddUntilStep("get skin editor", () => (skinEditor = Game.ChildrenOfType().FirstOrDefault()) != null); + AddUntilStep("skin editor loaded", () => skinEditor != null); } [Test] public void TestEditComponentDuringGameplay() { + advanceToSongSelect(); + openSkinEditor(); + switchToGameplayScene(); BarHitErrorMeter hitErrorMeter = null; @@ -85,6 +89,8 @@ public void TestEditComponentDuringGameplay() [Test] public void TestAutoplayCompatibleModsRetainedOnEnteringGameplay() { + advanceToSongSelect(); + openSkinEditor(); AddStep("select DT", () => Game.SelectedMods.Value = new Mod[] { new OsuModDoubleTime() }); switchToGameplayScene(); @@ -95,6 +101,8 @@ public void TestAutoplayCompatibleModsRetainedOnEnteringGameplay() [Test] public void TestAutoplayIncompatibleModsRemovedOnEnteringGameplay() { + advanceToSongSelect(); + openSkinEditor(); AddStep("select no fail and spun out", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail(), new OsuModSpunOut() }); switchToGameplayScene(); @@ -105,6 +113,8 @@ public void TestAutoplayIncompatibleModsRemovedOnEnteringGameplay() [Test] public void TestDuplicateAutoplayModRemovedOnEnteringGameplay() { + advanceToSongSelect(); + openSkinEditor(); AddStep("select autoplay", () => Game.SelectedMods.Value = new Mod[] { new OsuModAutoplay() }); switchToGameplayScene(); @@ -115,6 +125,8 @@ public void TestDuplicateAutoplayModRemovedOnEnteringGameplay() [Test] public void TestCinemaModRemovedOnEnteringGameplay() { + advanceToSongSelect(); + openSkinEditor(); AddStep("select cinema", () => Game.SelectedMods.Value = new Mod[] { new OsuModCinema() }); switchToGameplayScene(); @@ -122,6 +134,16 @@ public void TestCinemaModRemovedOnEnteringGameplay() AddAssert("no mod selected", () => !((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Any()); } + [Test] + public void TestModOverlayClosesOnOpeningSkinEditor() + { + advanceToSongSelect(); + AddStep("open mod overlay", () => songSelect.ModSelectOverlay.Show()); + + openSkinEditor(); + AddUntilStep("mod overlay closed", () => songSelect.ModSelectOverlay.State.Value == Visibility.Hidden); + } + private void switchToGameplayScene() { AddStep("Click gameplay scene button", () => skinEditor.ChildrenOfType().First(b => b.Text == "Gameplay").TriggerClick()); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 4eed2a25f51c..a1d51683e4d1 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -56,6 +56,17 @@ protected override void LoadComplete() }); } + [Test] + public void TestScaling() + { + // scheduling is needed as scaling the content immediately causes the entire scene to shake badly, for some odd reason. + AddSliderStep("scale", 0.5f, 1.6f, 1f, v => Schedule(() => + { + Content.Scale = new Vector2(v); + Content.Size = new Vector2(1f / v); + })); + } + [Test] public void TestResultsWithoutPlayer() { diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index d27f16a624f5..083e24be7b14 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; +using osu.Framework.Graphics.Containers; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; @@ -18,6 +19,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; +using osu.Game.Overlays.Mods; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; @@ -918,6 +920,19 @@ public void TestChangeBeatmapWhilePresentingScore() AddAssert("check ruleset is correct for score", () => Ruleset.Value.OnlineID == 0); } + [Test] + public void TestModOverlayToggling() + { + changeRuleset(0); + createSongSelect(); + + AddStep("toggle mod overlay on", () => InputManager.Key(Key.F1)); + AddUntilStep("mod overlay shown", () => songSelect.ModSelect.State.Value == Visibility.Visible); + + AddStep("toggle mod overlay off", () => InputManager.Key(Key.F1)); + AddUntilStep("mod overlay hidden", () => songSelect.ModSelect.State.Value == Visibility.Hidden); + } + private void waitForInitialSelection() { AddUntilStep("wait for initial selection", () => !Beatmap.IsDefault); @@ -993,6 +1008,7 @@ private class TestSongSelect : PlaySongSelect public WorkingBeatmap CurrentBeatmap => Beatmap.Value; public IWorkingBeatmap CurrentBeatmapDetailsBeatmap => BeatmapDetails.Beatmap; public new BeatmapCarousel Carousel => base.Carousel; + public new ModSelectScreen ModSelect => base.ModSelect; public new void PresentScore(ScoreInfo score) => base.PresentScore(score); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs index c92a738b5634..fa7758df5954 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectScreen.cs @@ -415,6 +415,72 @@ public void TestUnimplementedModIsUnselectable() 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); + } + + [Test] + public void TestColumnHiding() + { + AddStep("create screen", () => Child = modSelectScreen = new UserModSelectScreen + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible }, + SelectedMods = { BindTarget = SelectedMods }, + IsValidMod = mod => mod.Type == ModType.DifficultyIncrease || mod.Type == ModType.Conversion + }); + waitForColumnLoad(); + changeRuleset(0); + + AddAssert("two columns visible", () => this.ChildrenOfType().Count(col => col.IsPresent) == 2); + + AddStep("unset filter", () => modSelectScreen.IsValidMod = _ => true); + AddAssert("all columns visible", () => this.ChildrenOfType().All(col => col.IsPresent)); + + AddStep("filter out everything", () => modSelectScreen.IsValidMod = _ => false); + AddAssert("no columns visible", () => this.ChildrenOfType().All(col => !col.IsPresent)); + + AddStep("hide", () => modSelectScreen.Hide()); + AddStep("set filter for 3 columns", () => modSelectScreen.IsValidMod = mod => mod.Type == ModType.DifficultyReduction + || mod.Type == ModType.Automation + || mod.Type == ModType.Conversion); + + AddStep("show", () => modSelectScreen.Show()); + AddUntilStep("3 columns visible", () => this.ChildrenOfType().Count(col => col.IsPresent) == 3); + } + private void waitForColumnLoad() => AddUntilStep("all column content loaded", () => modSelectScreen.ChildrenOfType().Any() && modSelectScreen.ChildrenOfType().All(column => column.IsLoaded && column.ItemsLoaded)); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedOverlayContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedOverlayContainer.cs index 5a9cafde278b..4dd64c6536df 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneShearedOverlayContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneShearedOverlayContainer.cs @@ -66,7 +66,10 @@ public void TestClickAwayToExit() public class TestShearedOverlayContainer : ShearedOverlayContainer { - protected override OverlayColourScheme ColourScheme => OverlayColourScheme.Green; + public TestShearedOverlayContainer() + : base(OverlayColourScheme.Green) + { + } [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Screens/Play/ISamplePlaybackDisabler.cs b/osu.Game/Audio/ISamplePlaybackDisabler.cs similarity index 72% rename from osu.Game/Screens/Play/ISamplePlaybackDisabler.cs rename to osu.Game/Audio/ISamplePlaybackDisabler.cs index 6b37021fe6ab..4167316780a4 100644 --- a/osu.Game/Screens/Play/ISamplePlaybackDisabler.cs +++ b/osu.Game/Audio/ISamplePlaybackDisabler.cs @@ -1,15 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Skinning; -namespace osu.Game.Screens.Play +namespace osu.Game.Audio { /// /// Allows a component to disable sample playback dynamically as required. - /// Handled by . + /// Automatically handled by . + /// May also be manually handled locally to particular components. /// + [Cached] public interface ISamplePlaybackDisabler { /// diff --git a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs index 46f5b418bdd3..fc39887e79d0 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs @@ -153,7 +153,17 @@ private void prepareLocalCache() } }; - Task.Run(() => cacheDownloadRequest.PerformAsync()); + Task.Run(async () => + { + try + { + await cacheDownloadRequest.PerformAsync(); + } + catch + { + // Prevent throwing unobserved exceptions, as they will be logged from the network request to the log file anyway. + } + }); } private bool checkLocalCache(BeatmapSetInfo set, BeatmapInfo beatmapInfo) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 79d8bd3bb3a1..3a893a1238c6 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -431,9 +431,10 @@ private void handleTimingPoint(string line) OmitFirstBarLine = omitFirstBarSignature, }; - bool isOsuRuleset = beatmap.BeatmapInfo.Ruleset.OnlineID == 0; - // scrolling rulesets use effect points rather than difficulty points for scroll speed adjustments. - if (!isOsuRuleset) + int onlineRulesetID = beatmap.BeatmapInfo.Ruleset.OnlineID; + + // osu!taiko and osu!mania use effect points rather than difficulty points for scroll speed adjustments. + if (onlineRulesetID == 1 || onlineRulesetID == 3) effectPoint.ScrollSpeed = speedMultiplier; addControlPoint(time, effectPoint, timingChange); diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 7ddbc2f7684c..b91a74c4a19b 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -183,15 +183,15 @@ private void handleControlPoints(TextWriter writer) SampleControlPoint lastRelevantSamplePoint = null; DifficultyControlPoint lastRelevantDifficultyPoint = null; - bool isOsuRuleset = onlineRulesetID == 0; + // In osu!taiko and osu!mania, a scroll speed is stored as "slider velocity" in legacy formats. + // In that case, a scrolling speed change is a global effect and per-hit object difficulty control points are ignored. + bool scrollSpeedEncodedAsSliderVelocity = onlineRulesetID == 1 || onlineRulesetID == 3; // iterate over hitobjects and pull out all required sample and difficulty changes extractDifficultyControlPoints(beatmap.HitObjects); extractSampleControlPoints(beatmap.HitObjects); - // handle scroll speed, which is stored as "slider velocity" in legacy formats. - // this is relevant for scrolling ruleset beatmaps. - if (!isOsuRuleset) + if (scrollSpeedEncodedAsSliderVelocity) { foreach (var point in legacyControlPoints.EffectPoints) legacyControlPoints.Add(point.Time, new DifficultyControlPoint { SliderVelocity = point.ScrollSpeed }); @@ -242,7 +242,7 @@ void outputControlPointAt(double time, bool isTimingPoint) IEnumerable collectDifficultyControlPoints(IEnumerable hitObjects) { - if (!isOsuRuleset) + if (scrollSpeedEncodedAsSliderVelocity) yield break; foreach (var hitObject in hitObjects) diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index 68351acd7ef5..512602d12003 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -34,7 +34,7 @@ public abstract class OsuFocusedOverlayContainer : FocusedOverlayContainer, IPre protected virtual bool DimMainContent => true; [Resolved(CanBeNull = true)] - private OsuGame game { get; set; } + private IOverlayManager overlayManager { get; set; } [Resolved] private PreviewTrackManager previewTrackManager { get; set; } @@ -50,8 +50,8 @@ private void load(AudioManager audio) protected override void LoadComplete() { - if (game != null) - OverlayActivationMode.BindTo(game.OverlayActivationMode); + if (overlayManager != null) + OverlayActivationMode.BindTo(overlayManager.OverlayActivationMode); OverlayActivationMode.BindValueChanged(mode => { @@ -127,14 +127,14 @@ protected override void UpdateState(ValueChangedEvent state) if (didChange) samplePopIn?.Play(); - if (BlockScreenWideMouse && DimMainContent) game?.AddBlockingOverlay(this); + if (BlockScreenWideMouse && DimMainContent) overlayManager?.ShowBlockingOverlay(this); break; case Visibility.Hidden: if (didChange) samplePopOut?.Play(); - if (BlockScreenWideMouse) game?.RemoveBlockingOverlay(this); + if (BlockScreenWideMouse) overlayManager?.HideBlockingOverlay(this); break; } @@ -150,7 +150,7 @@ protected override void PopOut() protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - game?.RemoveBlockingOverlay(this); + overlayManager?.HideBlockingOverlay(this); } } } diff --git a/osu.Game/Localisation/CommonStrings.cs b/osu.Game/Localisation/CommonStrings.cs index 52e9811cf712..9cd626af0f54 100644 --- a/osu.Game/Localisation/CommonStrings.cs +++ b/osu.Game/Localisation/CommonStrings.cs @@ -59,6 +59,16 @@ public static class CommonStrings /// 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 000000000000..c281d90190aa --- /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 000000000000..0c113fd381cb --- /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 9b6881f98ccd..08ab64fd0831 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 c9fc59c5a29d..4ec3952941a5 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 084ae6794008..6aba12f86fc1 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 09cf0af89c53..5688455f79a9 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 166d3c086d90..1feb37531f0f 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 5564a30bbf8c..8646d1d76b6f 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 a0270ab0fdb6..319748bed687 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 27269cc5fc78..91eabe886809 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 71304ea979ab..d888ccd5a20b 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 506d65f76167..fdea636ac633 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 bba4944bb747..bb147dff84f8 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 274b8030a94a..30f27043a0e3 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 860264a7dd44..ee825a1e9cb8 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 2b6f94c5a4d1..58980132f3fe 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 0720e0eac770..f2eef600dc26 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 99237419b73e..7779b55bb799 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 55dc18b6a3c4..0620a0624fda 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 f5963ebf5e35..f8ce354aa14b 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 9ed0943acd7f..af82b4db2076 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 ec4475971c31..3d2ddbf6fc3e 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 be6968aa5dc9..58a35a7bf375 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 155d6670a820..4d3941dd20eb 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 1d5b0769a4db..887635fa8509 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 98fe9b5e136a..7b579e27b9eb 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 564f5f45209f..d1b09e2c1ded 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 bf3f855d5ff0..f6fc1f442042 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/OsuGame.cs b/osu.Game/OsuGame.cs index e9fe8c43de0d..54c4231b06e2 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -63,7 +63,7 @@ namespace osu.Game /// The full osu! experience. Builds on top of to add menus and binding logic /// for initial components that are generally retrieved via DI. /// - public class OsuGame : OsuGameBase, IKeyBindingHandler, ILocalUserPlayInfo, IPerformFromScreenRunner + public class OsuGame : OsuGameBase, IKeyBindingHandler, ILocalUserPlayInfo, IPerformFromScreenRunner, IOverlayManager { /// /// The amount of global offset to apply when a left/right anchored overlay is displayed (ie. settings or notifications). @@ -171,6 +171,7 @@ public class OsuGame : OsuGameBase, IKeyBindingHandler, ILocalUser private readonly string[] args; private readonly List focusedOverlays = new List(); + private readonly List externalOverlays = new List(); private readonly List visibleBlockingOverlays = new List(); @@ -183,22 +184,58 @@ public OsuGame(string[] args = null) SentryLogger = new SentryLogger(this); } + #region IOverlayManager + + IBindable IOverlayManager.OverlayActivationMode => OverlayActivationMode; + private void updateBlockingOverlayFade() => ScreenContainer.FadeColour(visibleBlockingOverlays.Any() ? OsuColour.Gray(0.5f) : Color4.White, 500, Easing.OutQuint); - public void AddBlockingOverlay(OverlayContainer overlay) + IDisposable IOverlayManager.RegisterBlockingOverlay(OverlayContainer overlayContainer) + { + if (overlayContainer.Parent != null) + throw new ArgumentException($@"Overlays registered via {nameof(IOverlayManager.RegisterBlockingOverlay)} should not be added to the scene graph."); + + if (externalOverlays.Contains(overlayContainer)) + throw new ArgumentException($@"{overlayContainer} has already been registered via {nameof(IOverlayManager.RegisterBlockingOverlay)} once."); + + externalOverlays.Add(overlayContainer); + overlayContent.Add(overlayContainer); + + if (overlayContainer is OsuFocusedOverlayContainer focusedOverlayContainer) + focusedOverlays.Add(focusedOverlayContainer); + + return new InvokeOnDisposal(() => unregisterBlockingOverlay(overlayContainer)); + } + + void IOverlayManager.ShowBlockingOverlay(OverlayContainer overlay) { if (!visibleBlockingOverlays.Contains(overlay)) visibleBlockingOverlays.Add(overlay); updateBlockingOverlayFade(); } - public void RemoveBlockingOverlay(OverlayContainer overlay) => Schedule(() => + void IOverlayManager.HideBlockingOverlay(OverlayContainer overlay) => Schedule(() => { visibleBlockingOverlays.Remove(overlay); updateBlockingOverlayFade(); }); + /// + /// Unregisters a blocking that was not created by itself. + /// + private void unregisterBlockingOverlay(OverlayContainer overlayContainer) + { + externalOverlays.Remove(overlayContainer); + + if (overlayContainer is OsuFocusedOverlayContainer focusedOverlayContainer) + focusedOverlays.Remove(focusedOverlayContainer); + + overlayContainer.Expire(); + } + + #endregion + /// /// Close all game-wide overlays. /// @@ -1153,6 +1190,7 @@ protected override void UpdateAfterChildren() 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 9ee002fd9d32..f528f54cdf75 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -119,7 +119,7 @@ public ScoreInfo Score 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/Dialog/PopupDialog.cs b/osu.Game/Overlays/Dialog/PopupDialog.cs index d08b6b7bebec..5959fe656c4d 100644 --- a/osu.Game/Overlays/Dialog/PopupDialog.cs +++ b/osu.Game/Overlays/Dialog/PopupDialog.cs @@ -100,10 +100,6 @@ public IEnumerable Buttons } } - // We always want dialogs to show their appear animation, so we request they start hidden. - // Normally this would not be required, but is here due to the manual Show() call that occurs before LoadComplete(). - protected override bool StartHidden => true; - protected PopupDialog() { RelativeSizeAxes = Axes.Both; @@ -272,7 +268,7 @@ protected override void PopIn() protected override void PopOut() { - if (!actionInvoked && content.IsPresent) + if (!actionInvoked) // In the case a user did not choose an action before a hide was triggered, press the last button. // This is presumed to always be a sane default "cancel" action. buttonsContainer.Last().TriggerClick(); diff --git a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs index 862506add2ba..152d67ab2750 100644 --- a/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs +++ b/osu.Game/Overlays/FirstRunSetup/ScreenUIScale.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -19,6 +20,7 @@ using osu.Game.Localisation; using osu.Game.Overlays.Settings; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Screens; using osu.Game.Screens.Menu; using osu.Game.Screens.Select; @@ -131,6 +133,10 @@ private class SampleScreenContainer : CompositeDrawable [Cached(typeof(IBindable))] protected Bindable Beatmap { get; private set; } = new Bindable(); + [Cached] + [Cached(typeof(IBindable>))] + protected Bindable> SelectedMods { get; private set; } = new Bindable>(Array.Empty()); + public override bool HandlePositionalInput => false; public override bool HandleNonPositionalInput => false; public override bool PropagatePositionalInputSubTree => false; diff --git a/osu.Game/Overlays/FirstRunSetupOverlay.cs b/osu.Game/Overlays/FirstRunSetupOverlay.cs index 75778e6c4d9f..607bef76ddfc 100644 --- a/osu.Game/Overlays/FirstRunSetupOverlay.cs +++ b/osu.Game/Overlays/FirstRunSetupOverlay.cs @@ -31,8 +31,6 @@ namespace osu.Game.Overlays [Cached] public class FirstRunSetupOverlay : ShearedOverlayContainer { - protected override OverlayColourScheme ColourScheme => OverlayColourScheme.Purple; - [Resolved] private IPerformFromScreenRunner performer { get; set; } = null!; @@ -70,6 +68,11 @@ public class FirstRunSetupOverlay : ShearedOverlayContainer private Container content = null!; + public FirstRunSetupOverlay() + : base(OverlayColourScheme.Purple) + { + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { diff --git a/osu.Game/Overlays/IOverlayManager.cs b/osu.Game/Overlays/IOverlayManager.cs new file mode 100644 index 000000000000..940ee2d8dbd9 --- /dev/null +++ b/osu.Game/Overlays/IOverlayManager.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Containers; +using osu.Game.Screens.Select; + +namespace osu.Game.Overlays +{ + [Cached] + internal interface IOverlayManager + { + /// + /// Whether overlays should be able to be opened game-wide. Value is sourced from the current active screen. + /// + IBindable OverlayActivationMode { get; } + + /// + /// Registers a blocking that was not created by itself for later use. + /// + /// + /// The goal of this method is to allow child screens, like to register their own full-screen blocking overlays + /// with background dim. + /// In those cases, for the dim to work correctly, the overlays need to be added at a game level directly, rather as children of the screens. + /// + /// + /// An that should be disposed of when the should be unregistered. + /// Disposing of this will automatically expire the . + /// + IDisposable RegisterBlockingOverlay(OverlayContainer overlayContainer); + + /// + /// Should be called when has been shown and should begin blocking background input. + /// + void ShowBlockingOverlay(OverlayContainer overlay); + + /// + /// Should be called when a blocking has been hidden and should stop blocking background input. + /// + void HideBlockingOverlay(OverlayContainer overlay); + } +} diff --git a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs index 1d848fe45612..4ccec0dd87b2 100644 --- a/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs +++ b/osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs @@ -15,6 +15,7 @@ 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 @@ public DifficultyMultiplierDisplay() 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 a792c0a81e8f..3a2fda0bb0ae 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.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; @@ -39,7 +41,7 @@ public class ModColumn : CompositeDrawable private Func? filter; /// - /// Function determining whether each mod in the column should be displayed. + /// A function determining whether each mod in the column should be displayed. /// A return value of means that the mod is not filtered and therefore its corresponding panel should be displayed. /// A return value of means that the mod is filtered out and therefore its corresponding panel should be hidden. /// @@ -49,12 +51,22 @@ public Func? Filter set { filter = value; - updateFilter(); + updateState(); } } + /// + /// Determines whether this column should accept user input. + /// public Bindable Active = new BindableBool(true); + private readonly Bindable allFiltered = new BindableBool(); + + /// + /// True if all of the panels in this column have been filtered out by the current . + /// + public IBindable AllFiltered => allFiltered; + /// /// List of mods marked as selected in this column. /// @@ -186,7 +198,7 @@ public ModColumn(ModType modType, bool allowBulkSelection, Key[]? toggleKeys = n }, new Drawable[] { - new NestedVerticalScrollContainer + new OsuScrollContainer(Direction.Vertical) { RelativeSizeAxes = Axes.Both, ClampExtension = 100, @@ -220,7 +232,6 @@ public ModColumn(ModType modType, bool allowBulkSelection, Key[]? toggleKeys = n Origin = Anchor.CentreLeft, Scale = new Vector2(0.8f), RelativeSizeAxes = Axes.X, - LabelText = "Enable All", Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0) }); panelFlow.Padding = new MarginPadding @@ -249,9 +260,8 @@ private void createHeaderText() private void load(OsuGameBase game, OverlayColourProvider colourProvider, OsuColour colours) { availableMods.BindTo(game.AvailableMods); - // this `BindValueChanged` callback is intentionally here, to ensure that local available mods are constructed as early as possible. - // this is needed to make sure no external changes to mods are dropped while mod panels are asynchronously loading. - availableMods.BindValueChanged(_ => updateLocalAvailableMods(), true); + updateLocalAvailableMods(asyncLoadContent: false); + availableMods.BindValueChanged(_ => updateLocalAvailableMods(asyncLoadContent: true)); headerBackground.Colour = accentColour = colours.ForModType(ModType); @@ -265,7 +275,20 @@ private void load(OsuGameBase game, OverlayColourProvider colourProvider, OsuCol contentBackground.Colour = colourProvider.Background4; } - private void updateLocalAvailableMods() + 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(bool asyncLoadContent) { var newMods = ModUtils.FlattenMods(availableMods.Value.GetValueOrDefault(ModType) ?? Array.Empty()) .Select(m => m.DeepClone()) @@ -275,32 +298,24 @@ private void updateLocalAvailableMods() return; localAvailableMods = newMods; - Scheduler.AddOnce(loadPanels); + + if (asyncLoadContent) + asyncLoadPanels(); + else + onPanelsLoaded(createPanels()); } 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 +323,39 @@ private void loadPanels() }); } - 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); + } + + allFiltered.Value = panelFlow.All(panel => panel.Filtered.Value); + + 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 +367,16 @@ private void updateActiveState() 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 +409,7 @@ internal void SetSelection(IReadOnlyList mods) } SelectedMods = newSelection; - updateActiveState(); + updateState(); externalSelectionUpdateInProgress = false; } @@ -378,7 +423,7 @@ internal void SetSelection(IReadOnlyList 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 +448,6 @@ protected override void Update() } } - 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 +543,6 @@ protected override void OnUserChange(bool value) #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/ModPanel.cs b/osu.Game/Overlays/Mods/ModPanel.cs index f2a97da3b258..4c4951307dcd 100644 --- a/osu.Game/Overlays/Mods/ModPanel.cs +++ b/osu.Game/Overlays/Mods/ModPanel.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -12,6 +14,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Utils; +using osu.Game.Audio; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -21,8 +24,6 @@ using osuTK; using osuTK.Input; -#nullable enable - namespace osu.Game.Overlays.Mods { public class ModPanel : OsuClickableContainer @@ -50,6 +51,7 @@ public class ModPanel : OsuClickableContainer private Colour4 activeColour; + private readonly Bindable samplePlaybackDisabled = new BindableBool(); private Sample? sampleOff; private Sample? sampleOn; @@ -139,13 +141,16 @@ public ModPanel(Mod mod) Action = Active.Toggle; } - [BackgroundDependencyLoader] - private void load(AudioManager audio, OsuColour colours) + [BackgroundDependencyLoader(true)] + private void load(AudioManager audio, OsuColour colours, ISamplePlaybackDisabler? samplePlaybackDisabler) { sampleOn = audio.Samples.Get(@"UI/check-on"); sampleOff = audio.Samples.Get(@"UI/check-off"); activeColour = colours.ForModType(Mod.Type); + + if (samplePlaybackDisabler != null) + ((IBindable)samplePlaybackDisabled).BindTo(samplePlaybackDisabler.SamplePlaybackDisabled); } protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet); @@ -166,6 +171,9 @@ protected override void LoadComplete() private void playStateChangeSamples() { + if (samplePlaybackDisabled.Value) + return; + if (Active.Value) sampleOn?.Play(); else diff --git a/osu.Game/Overlays/Mods/ModSelectScreen.cs b/osu.Game/Overlays/Mods/ModSelectScreen.cs index a1b4d7d8a0e9..8b19e389543a 100644 --- a/osu.Game/Overlays/Mods/ModSelectScreen.cs +++ b/osu.Game/Overlays/Mods/ModSelectScreen.cs @@ -13,27 +13,35 @@ 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.Audio; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; +using osu.Game.Localisation; using osu.Game.Rulesets.Mods; using osuTK; using osuTK.Input; namespace osu.Game.Overlays.Mods { - public abstract class ModSelectScreen : ShearedOverlayContainer + public abstract class ModSelectScreen : ShearedOverlayContainer, ISamplePlaybackDisabler { - protected override OverlayColourScheme ColourScheme => OverlayColourScheme.Green; + protected const int BUTTON_WIDTH = 200; [Cached] public Bindable> SelectedMods { get; private set; } = new Bindable>(Array.Empty()); private Func isValidMod = m => true; + /// + /// A function determining whether each mod in the column should be displayed. + /// A return value of means that the mod is not filtered and therefore its corresponding panel should be displayed. + /// A return value of means that the mod is filtered out and therefore its corresponding panel should be hidden. + /// public Func IsValidMod { get => isValidMod; @@ -46,11 +54,6 @@ public Func IsValidMod } } - /// - /// 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. /// @@ -58,18 +61,32 @@ public Func IsValidMod protected virtual ModColumn CreateModColumn(ModType modType, Key[]? toggleKeys = null) => new ModColumn(modType, false, toggleKeys); + protected virtual IReadOnlyList ComputeNewModsFromSelection(IReadOnlyList oldSelection, IReadOnlyList newSelection) => newSelection; + + protected virtual IEnumerable CreateFooterButtons() => createDefaultFooterButtons(); + private readonly BindableBool customisationVisible = new BindableBool(); - private DifficultyMultiplierDisplay? multiplierDisplay; private ModSettingsArea modSettingsArea = null!; private ColumnScrollContainer columnScroll = null!; private ColumnFlowContainer columnFlow = null!; + private FillFlowContainer footerButtonFlow = null!; + private ShearedButton backButton = null!; + + private DifficultyMultiplierDisplay? multiplierDisplay; + + private ShearedToggleButton? customisationButton; + + protected ModSelectScreen(OverlayColourScheme colourScheme = OverlayColourScheme.Green) + : base(colourScheme) + { + } [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[] { @@ -94,6 +111,7 @@ private void load() Padding = new MarginPadding { Top = (ShowTotalMultiplier ? DifficultyMultiplierDisplay.HEIGHT : 0) + PADDING, + Bottom = PADDING }, RelativeSizeAxes = Axes.Both, RelativePositionAxes = Axes.Both, @@ -111,7 +129,6 @@ private void load() Shear = new Vector2(SHEAR, 0), RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, - Spacing = new Vector2(10, 0), Margin = new MarginPadding { Horizontal = 70 }, Children = new[] { @@ -144,31 +161,35 @@ private void load() }); } - 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 } - }); - } - } - - private ColumnDimContainer createModColumnContent(ModType modType, Key[]? toggleKeys = null) - => new ColumnDimContainer(CreateModColumn(modType, toggleKeys)) - { - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - RequestScroll = column => columnScroll.ScrollIntoView(column, extraScroll: 140) + Vertical = PADDING, + Horizontal = 70 + }, + Spacing = new Vector2(10), + ChildrenEnumerable = CreateFooterButtons().Prepend(backButton = new ShearedButton(BUTTON_WIDTH) + { + Text = CommonStrings.Back, + Action = Hide, + DarkerColour = colours.Pink2, + LighterColour = colours.Pink1 + }) }; + } protected override void LoadComplete() { base.LoadComplete(); + State.BindValueChanged(_ => samplePlaybackDisabled.Value = State.Value == Visibility.Hidden, true); + ((IBindable>)modSettingsArea.SelectedMods).BindTo(SelectedMods); SelectedMods.BindValueChanged(val => @@ -186,8 +207,66 @@ protected override void LoadComplete() customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true); updateAvailableMods(); + + // Start scrolled slightly to the right to give the user a sense that + // there is more horizontal content available. + ScheduleAfterChildren(() => + { + columnScroll.ScrollTo(200, false); + columnScroll.ScrollToStart(); + }); + } + + /// + /// Select all visible mods in all columns. + /// + protected void SelectAll() + { + foreach (var column in columnFlow.Columns) + column.SelectAll(); + } + + /// + /// Deselect all visible mods in all columns. + /// + protected void DeselectAll() + { + foreach (var column in columnFlow.Columns) + column.DeselectAll(); + } + + private ColumnDimContainer createModColumnContent(ModType modType, Key[]? toggleKeys = null) + { + var column = CreateModColumn(modType, toggleKeys).With(column => + { + column.Filter = IsValidMod; + // spacing applied here rather than via `columnFlow.Spacing` to avoid uneven gaps when some of the columns are hidden. + column.Margin = new MarginPadding { Right = 10 }; + }); + + return new ColumnDimContainer(column) + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + RequestScroll = col => columnScroll.ScrollIntoView(col, extraScroll: 140), + }; } + private ShearedButton[] createDefaultFooterButtons() + => new[] + { + customisationButton = new ShearedToggleButton(BUTTON_WIDTH) + { + Text = ModSelectScreenStrings.ModCustomisation, + Active = { BindTarget = customisationVisible } + }, + new ShearedButton(BUTTON_WIDTH) + { + Text = CommonStrings.DeselectAll, + Action = DeselectAll + } + }; + private void updateMultiplier() { if (multiplierDisplay == null) @@ -209,7 +288,7 @@ private void updateAvailableMods() private void updateCustomisation(ValueChangedEvent> valueChangedEvent) { - if (!AllowCustomisation) + if (customisationButton == null) return; bool anyCustomisableMod = false; @@ -243,6 +322,12 @@ private void updateCustomisationVisualState() 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); @@ -264,13 +349,17 @@ private void updateBindableFromSelection() { 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); } - protected virtual IReadOnlyList ComputeNewModsFromSelection(IReadOnlyList oldSelection, IReadOnlyList newSelection) => newSelection; + #region Transition handling + + private const float distance = 700; protected override void PopIn() { @@ -283,13 +372,26 @@ protected override void PopIn() .FadeIn(fade_in_duration / 2, Easing.OutQuint) .ScaleTo(1, fade_in_duration, Easing.OutElastic); + int nonFilteredColumnCount = 0; + for (int i = 0; i < columnFlow.Count; i++) { - columnFlow[i].Column - .TopLevelContent - .Delay(i * 30) - .MoveToY(0, fade_in_duration, Easing.OutQuint) - .FadeIn(fade_in_duration, Easing.OutQuint); + var column = columnFlow[i].Column; + + double delay = column.AllFiltered.Value ? 0 : nonFilteredColumnCount * 30; + double duration = column.AllFiltered.Value ? 0 : fade_in_duration; + float startingYPosition = 0; + if (!column.AllFiltered.Value) + startingYPosition = nonFilteredColumnCount % 2 == 0 ? -distance : distance; + + column.TopLevelContent + .MoveToY(startingYPosition) + .Delay(delay) + .MoveToY(0, duration, Easing.OutQuint) + .FadeIn(duration, Easing.OutQuint); + + if (!column.AllFiltered.Value) + nonFilteredColumnCount += 1; } } @@ -303,30 +405,83 @@ protected override void PopOut() .FadeOut(fade_out_duration / 2, Easing.OutQuint) .ScaleTo(0.75f, fade_out_duration, Easing.OutQuint); + int nonFilteredColumnCount = 0; + for (int i = 0; i < columnFlow.Count; i++) { - const float distance = 700; - var column = columnFlow[i].Column; + double duration = column.AllFiltered.Value ? 0 : fade_out_duration; + float newYPosition = 0; + if (!column.AllFiltered.Value) + newYPosition = nonFilteredColumnCount % 2 == 0 ? -distance : distance; + column.FlushPendingSelections(); column.TopLevelContent - .MoveToY(i % 2 == 0 ? -distance : distance, fade_out_duration, Easing.OutQuint) - .FadeOut(fade_out_duration, Easing.OutQuint); + .MoveToY(newYPosition, duration, Easing.OutQuint) + .FadeOut(duration, Easing.OutQuint); + + if (!column.AllFiltered.Value) + nonFilteredColumnCount += 1; } } + #endregion + + #region Input handling + public override bool OnPressed(KeyBindingPressEvent e) { - if (e.Action == GlobalAction.Back && customisationVisible.Value) + if (e.Repeat) + return false; + + switch (e.Action) { - customisationVisible.Value = false; - return true; + case GlobalAction.Back: + // Pressing the back binding should only go back one step at a time. + hideOverlay(false); + return true; + + // This is handled locally here because this overlay is being registered at the game level + // and therefore takes away keyboard focus from the screen stack. + case GlobalAction.ToggleModSelection: + case GlobalAction.Select: + { + // Pressing toggle or select should completely hide the overlay in one shot. + hideOverlay(true); + return true; + } } return base.OnPressed(e); + + void hideOverlay(bool immediate) + { + if (customisationVisible.Value) + { + Debug.Assert(customisationButton != null); + customisationButton.TriggerClick(); + + if (!immediate) + return; + } + + backButton.TriggerClick(); + } } + #endregion + + #region Sample playback control + + private readonly Bindable samplePlaybackDisabled = new BindableBool(true); + IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled; + + #endregion + + /// + /// Manages horizontal scrolling of mod columns, along with the "active" states of each column based on visibility. + /// internal class ColumnScrollContainer : OsuScrollContainer { public ColumnScrollContainer() @@ -365,6 +520,9 @@ protected override void Update() } } + /// + /// Manages padding and layout of mod columns. + /// internal class ColumnFlowContainer : FillFlowContainer { public IEnumerable Columns => Children.Select(dimWrapper => dimWrapper.Column); @@ -401,11 +559,21 @@ protected override void Update() } } + /// + /// Encapsulates a column and provides dim and input blocking based on an externally managed "active" state. + /// internal class ColumnDimContainer : Container { public ModColumn Column { get; } + /// + /// Tracks whether this column is in an interactive state. Generally only the case when the column is on-screen. + /// public readonly Bindable Active = new BindableBool(); + + /// + /// Invoked when the column is clicked while not active, requesting a scroll to be performed to bring it on-screen. + /// public Action? RequestScroll { get; set; } [Resolved] @@ -420,15 +588,20 @@ public ColumnDimContainer(ModColumn column) protected override void LoadComplete() { base.LoadComplete(); - Active.BindValueChanged(_ => updateDim(), true); + Active.BindValueChanged(_ => updateState()); + Column.AllFiltered.BindValueChanged(_ => updateState(), true); FinishTransforms(); } - private void updateDim() + protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate || Column.SelectionAnimationRunning; + + private void updateState() { Colour4 targetColour; - if (Active.Value) + Column.Alpha = Column.AllFiltered.Value ? 0 : 1; + + if (Column.Active.Value) targetColour = Colour4.White; else targetColour = IsHovered ? colours.GrayC : colours.Gray8; @@ -447,17 +620,20 @@ protected override bool OnClick(ClickEvent e) protected override bool OnHover(HoverEvent e) { base.OnHover(e); - updateDim(); + updateState(); return Active.Value; } protected override void OnHoverLost(HoverLostEvent e) { base.OnHoverLost(e); - updateDim(); + updateState(); } } + /// + /// A container which blocks and handles input, managing the "return from customisation" state change. + /// private class ClickToReturnContainer : Container { public BindableBool HandleMouse { get; } = new BindableBool(); diff --git a/osu.Game/Overlays/Mods/ModSettingsArea.cs b/osu.Game/Overlays/Mods/ModSettingsArea.cs index 08f563f93e14..f44e4bf07f2b 100644 --- a/osu.Game/Overlays/Mods/ModSettingsArea.cs +++ b/osu.Game/Overlays/Mods/ModSettingsArea.cs @@ -158,7 +158,7 @@ public ModSettingsColumn(Mod mod, IEnumerable settingsControls) new[] { Empty() }, new Drawable[] { - new NestedVerticalScrollContainer + new OsuScrollContainer(Direction.Vertical) { RelativeSizeAxes = Axes.Both, ClampExtension = 100, diff --git a/osu.Game/Overlays/Mods/NestedVerticalScrollContainer.cs b/osu.Game/Overlays/Mods/NestedVerticalScrollContainer.cs deleted file mode 100644 index d27f97f3d232..000000000000 --- a/osu.Game/Overlays/Mods/NestedVerticalScrollContainer.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable enable - -using osu.Framework.Graphics; -using osu.Framework.Input.Events; -using osu.Game.Graphics.Containers; - -namespace osu.Game.Overlays.Mods -{ - /// - /// A scroll container that handles the case of vertically scrolling content inside a larger horizontally scrolling parent container. - /// - public class NestedVerticalScrollContainer : OsuScrollContainer - { - private ModSelectScreen.ColumnScrollContainer? parentScrollContainer; - - protected override void LoadComplete() - { - base.LoadComplete(); - - parentScrollContainer = this.FindClosestParent(); - } - - protected override bool OnScroll(ScrollEvent e) - { - if (parentScrollContainer == null) - return base.OnScroll(e); - - bool topRightInView = parentScrollContainer.ScreenSpaceDrawQuad.Contains(ScreenSpaceDrawQuad.TopRight); - bool bottomLeftInView = parentScrollContainer.ScreenSpaceDrawQuad.Contains(ScreenSpaceDrawQuad.BottomLeft); - - // If not completely on-screen, handle scroll but also allow parent to scroll at the same time (to hopefully bring our content into full view). - if (!topRightInView || !bottomLeftInView) - return false; - - bool scrollingPastEnd = e.ScrollDelta.Y < 0 && IsScrolledToEnd(); - bool scrollingPastStart = e.ScrollDelta.Y > 0 && Target <= 0; - - // If at either of our extents, delegate scroll to the horizontal parent container. - if (scrollingPastStart || scrollingPastEnd) - return false; - - return base.OnScroll(e); - } - } -} diff --git a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs index eca192c8e579..92e88bfaaf0b 100644 --- a/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs +++ b/osu.Game/Overlays/Mods/ShearedOverlayContainer.cs @@ -51,17 +51,15 @@ public abstract class ShearedOverlayContainer : OsuFocusedOverlayContainer /// protected Container FooterContent { get; private set; } - protected abstract OverlayColourScheme ColourScheme { get; } - protected override bool StartHidden => true; protected override bool BlockNonPositionalInput => true; - protected ShearedOverlayContainer() + protected ShearedOverlayContainer(OverlayColourScheme colourScheme) { RelativeSizeAxes = Axes.Both; - ColourProvider = new OverlayColourProvider(ColourScheme); + ColourProvider = new OverlayColourProvider(colourScheme); } [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/Mods/UserModSelectScreen.cs b/osu.Game/Overlays/Mods/UserModSelectScreen.cs index ca33d356058e..a018797cbad4 100644 --- a/osu.Game/Overlays/Mods/UserModSelectScreen.cs +++ b/osu.Game/Overlays/Mods/UserModSelectScreen.cs @@ -12,6 +12,11 @@ namespace osu.Game.Overlays.Mods { public class UserModSelectScreen : ModSelectScreen { + public UserModSelectScreen(OverlayColourScheme colourScheme = OverlayColourScheme.Green) + : base(colourScheme) + { + } + protected override ModColumn CreateModColumn(ModType modType, Key[] toggleKeys = null) => new UserModColumn(modType, false, toggleKeys); protected override IReadOnlyList ComputeNewModsFromSelection(IReadOnlyList oldSelection, IReadOnlyList newSelection) diff --git a/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs b/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs index ec9cb550420b..d8eb5b65ac0a 100644 --- a/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs +++ b/osu.Game/Overlays/Profile/Header/Components/LevelProgressBar.cs @@ -62,7 +62,7 @@ private void load(OverlayColourProvider colourProvider) 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 48a4c31f3047..c05c160463ff 100644 --- a/osu.Game/Overlays/Rankings/SpotlightSelector.cs +++ b/osu.Game/Overlays/Rankings/SpotlightSelector.cs @@ -126,7 +126,7 @@ public void ShowInfo(GetSpotlightRankingsResponse response) 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 bdbd2942d1df..6c85ec275316 100644 --- a/osu.Game/Overlays/Rankings/Tables/PerformanceTable.cs +++ b/osu.Game/Overlays/Rankings/Tables/PerformanceTable.cs @@ -5,7 +5,6 @@ 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 @@ protected override RankingsTableColumn[] CreateUniqueHeaders() => new[] 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/Rulesets/Mods/IMod.cs b/osu.Game/Rulesets/Mods/IMod.cs index d5d1de91de2a..30fa1ea8cb40 100644 --- a/osu.Game/Rulesets/Mods/IMod.cs +++ b/osu.Game/Rulesets/Mods/IMod.cs @@ -39,6 +39,18 @@ public interface IMod : IEquatable /// bool UserPlayable { get; } + /// + /// Whether this mod is valid for multiplayer matches. + /// Should be false for mods that make gameplay duration dependent on user input (e.g. ). + /// + bool ValidForMultiplayer { get; } + + /// + /// Whether this mod is valid as a free mod in multiplayer matches. + /// Should be false for mods that affect the gameplay duration (e.g. and ). + /// + bool ValidForMultiplayerAsFreeMod { get; } + /// /// Create a fresh instance based on this mod. /// diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index b2d4be54ce75..af1550f8a909 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -94,6 +94,12 @@ public virtual string SettingDescription [JsonIgnore] public virtual bool UserPlayable => true; + [JsonIgnore] + public virtual bool ValidForMultiplayer => true; + + [JsonIgnore] + public virtual bool ValidForMultiplayerAsFreeMod => true; + [Obsolete("Going forward, the concept of \"ranked\" doesn't exist. The only exceptions are automation mods, which should now override and set UserPlayable to false.")] // Can be removed 20211009 public virtual bool Ranked => false; diff --git a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs index 1115b95e6fe0..93251f7b2ddb 100644 --- a/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs +++ b/osu.Game/Rulesets/Mods/ModAdaptiveSpeed.cs @@ -31,6 +31,9 @@ public class ModAdaptiveSpeed : Mod, IApplicableToRate, IApplicableToDrawableHit public override double ScoreMultiplier => 1; + public override bool ValidForMultiplayer => false; + public override bool ValidForMultiplayerAsFreeMod => false; + public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModTimeRamp) }; [SettingSource("Initial rate", "The starting speed of the track")] diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index 87dc627b1966..0ebe11b3934a 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -25,6 +25,8 @@ public abstract class ModAutoplay : Mod, IApplicableFailOverride, ICreateReplayD public bool RestartOnFail => false; public override bool UserPlayable => false; + public override bool ValidForMultiplayer => false; + public override bool ValidForMultiplayerAsFreeMod => false; public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail) }; diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index 88fb609c074a..05953f903f83 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Mods { public abstract class ModRateAdjust : Mod, IApplicableToRate { + public override bool ValidForMultiplayerAsFreeMod => false; + public abstract BindableNumber SpeedChange { get; } public virtual void ApplyToTrack(ITrack track) diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index b6b2decedeb7..fe6d54332c70 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -30,6 +30,8 @@ public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToBea [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] public abstract BindableBool AdjustPitch { get; } + public override bool ValidForMultiplayerAsFreeMod => false; + public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModAdaptiveSpeed) }; public override string SettingDescription => $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x"; diff --git a/osu.Game/Rulesets/Mods/UnknownMod.cs b/osu.Game/Rulesets/Mods/UnknownMod.cs index b426386d7a1c..72de0ad6538e 100644 --- a/osu.Game/Rulesets/Mods/UnknownMod.cs +++ b/osu.Game/Rulesets/Mods/UnknownMod.cs @@ -16,6 +16,8 @@ public class UnknownMod : Mod public override double ScoreMultiplier => 0; public override bool UserPlayable => false; + public override bool ValidForMultiplayer => false; + public override bool ValidForMultiplayerAsFreeMod => false; public override ModType Type => ModType.System; diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs index 91fad08aff4c..2c6bb766ad59 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs @@ -2,11 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Edit.Compose.Components { @@ -51,14 +55,12 @@ protected override void CreateContent() { float diameter = (i + 1) * DistanceBetweenTicks * 2; - AddInternal(new CircularProgress + AddInternal(new Ring(ReferenceObject, GetColourForIndexFromPlacement(i)) { - Origin = Anchor.Centre, Position = StartPosition, - Current = { Value = 1 }, + Origin = Anchor.Centre, Size = new Vector2(diameter), InnerRadius = 4 * 1f / diameter, - Colour = GetColourForIndexFromPlacement(i) }); } } @@ -100,5 +102,45 @@ public override (Vector2 position, double time) GetSnappedPosition(Vector2 posit return (snappedPosition, snappedTime); } + + private class Ring : CircularProgress + { + [Resolved] + private IDistanceSnapProvider snapProvider { get; set; } + + [Resolved(canBeNull: true)] + private EditorClock editorClock { get; set; } + + private readonly HitObject referenceObject; + + private readonly Color4 baseColour; + + public Ring(HitObject referenceObject, Color4 baseColour) + { + this.referenceObject = referenceObject; + + Colour = this.baseColour = baseColour; + + Current.Value = 1; + } + + protected override void Update() + { + base.Update(); + + if (editorClock == null) + return; + + float distanceSpacingMultiplier = (float)snapProvider.DistanceSpacingMultiplier.Value; + double timeFromReferencePoint = editorClock.CurrentTime - referenceObject.GetEndTime(); + + float distanceForCurrentTime = snapProvider.DurationToDistance(referenceObject, timeFromReferencePoint) + * distanceSpacingMultiplier; + + float timeBasedAlpha = 1 - Math.Clamp(Math.Abs(distanceForCurrentTime - Size.X / 2) / 30, 0, 1); + + Colour = baseColour.Opacity(Math.Max(baseColour.A, timeBasedAlpha)); + } + } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs index 2f39db06d441..1f64a50c02c4 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs @@ -4,14 +4,15 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Layout; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Edit.Compose.Components { @@ -135,7 +136,7 @@ protected override void Update() /// /// The 0-based beat index from the point of placement. /// The applicable colour. - protected ColourInfo GetColourForIndexFromPlacement(int placementIndex) + protected Color4 GetColourForIndexFromPlacement(int placementIndex) { var timingPoint = Beatmap.ControlPointInfo.TimingPointAt(StartTime); double beatLength = timingPoint.BeatLength / beatDivisor.Value; @@ -144,7 +145,7 @@ protected ColourInfo GetColourForIndexFromPlacement(int placementIndex) var colour = BindableBeatDivisor.GetColourFor(BindableBeatDivisor.GetDivisorForBeatIndex(beatIndex + placementIndex + 1, beatDivisor.Value), Colours); int repeatIndex = placementIndex / beatDivisor.Value; - return ColourInfo.SingleColour(colour).MultiplyAlpha(0.5f / (repeatIndex + 1)); + return colour.Opacity(0.5f / (repeatIndex + 1)); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs index 22479bd9b33b..f13ed0456a35 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxRotationHandle.cs @@ -100,7 +100,7 @@ private float convertDragEventToAngleOfRotation(DragEvent e) private void updateTooltipText() { - TooltipText = cumulativeRotation.Value?.ToLocalisableString("0.0°") ?? default(LocalisableString); + TooltipText = cumulativeRotation.Value?.ToLocalisableString("0.0°") ?? default; } } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3fde03358784..947c184009f8 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -19,6 +19,7 @@ using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Screens; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; @@ -50,7 +51,6 @@ namespace osu.Game.Screens.Edit { [Cached(typeof(IBeatSnapProvider))] - [Cached(typeof(ISamplePlaybackDisabler))] [Cached] public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler, IKeyBindingHandler, IBeatSnapProvider, ISamplePlaybackDisabler { diff --git a/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs b/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs index 613f16563cbb..f360a805996c 100644 --- a/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs +++ b/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs @@ -87,44 +87,46 @@ private Drawable getIconFor(MatchType matchType) }, }; - // 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 5a7a60b47900..e92de5e083ae 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectScreen.cs @@ -2,15 +2,19 @@ // See the LICENCE file in the repository root for full licence text. 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 @@ -20,10 +24,29 @@ public class FreeModSelectScreen : ModSelectScreen } public FreeModSelectScreen() + : base(OverlayColourScheme.Plum) { IsValidMod = _ => true; } protected override ModColumn CreateModColumn(ModType modType, Key[] toggleKeys = null) => new ModColumn(modType, true, toggleKeys); + + protected override IEnumerable CreateFooterButtons() => new[] + { + new ShearedButton(BUTTON_WIDTH) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Text = CommonStrings.SelectAll, + Action = SelectAll + }, + new ShearedButton(BUTTON_WIDTH) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Text = CommonStrings.DeselectAll, + Action = DeselectAll + } + }; } } diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index cc1f842f8c06..ec4e329f7a2b 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -57,6 +58,9 @@ public abstract class RoomSubScreen : OnlinePlaySubScreen, IPreviewTrackOwner protected readonly IBindable RoomId = new Bindable(); + [Resolved(CanBeNull = true)] + private IOverlayManager overlayManager { get; set; } + [Resolved] private MusicController music { get; set; } @@ -77,7 +81,11 @@ public abstract class RoomSubScreen : OnlinePlaySubScreen, IPreviewTrackOwner public readonly Room Room; private readonly bool allowEdit; - private ModSelectOverlay userModsSelectOverlay; + private ModSelectScreen userModsSelectOverlay; + + [CanBeNull] + private IDisposable userModsSelectOverlayRegistration; + private RoomSettingsOverlay settingsOverlay; private Drawable mainContent; @@ -180,11 +188,6 @@ private void load(AudioManager audio) Origin = Anchor.BottomLeft, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Child = userModsSelectOverlay = new UserModSelectOverlay - { - SelectedMods = { BindTarget = UserMods }, - IsValidMod = _ => false - } }, } } @@ -227,6 +230,12 @@ private void load(AudioManager audio) } } }; + + LoadComponent(userModsSelectOverlay = new UserModSelectScreen(OverlayColourScheme.Plum) + { + SelectedMods = { BindTarget = UserMods }, + IsValidMod = _ => false + }); } protected override void LoadComplete() @@ -254,6 +263,8 @@ protected override void LoadComplete() beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem); beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateWorkingBeatmap()); + + userModsSelectOverlayRegistration = overlayManager?.RegisterBlockingOverlay(userModsSelectOverlay); } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) @@ -298,7 +309,7 @@ public override void OnEntering(ScreenTransitionEvent e) public override void OnSuspending(ScreenTransitionEvent e) { - endHandlingTrack(); + onLeaving(); base.OnSuspending(e); } @@ -316,7 +327,7 @@ public override bool OnExiting(ScreenExitEvent e) RoomManager?.PartRoom(); Mods.Value = Array.Empty(); - endHandlingTrack(); + onLeaving(); return base.OnExiting(e); } @@ -412,6 +423,12 @@ private void beginHandlingTrack() Beatmap.BindValueChanged(applyLoopingToTrack, true); } + private void onLeaving() + { + userModsSelectOverlay.Hide(); + endHandlingTrack(); + } + private void endHandlingTrack() { Beatmap.ValueChanged -= applyLoopingToTrack; @@ -459,5 +476,12 @@ private void cancelTrackLooping() public class UserModSelectButton : PurpleTriangleButton { } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + userModsSelectOverlayRegistration?.Dispose(); + } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 848424bc765c..929c3ee32150 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -95,6 +95,8 @@ protected override void SelectItem(PlaylistItem item) protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); - protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && !(mod is ModTimeRamp) && !(mod is ModRateAdjust); + protected override bool IsValidMod(Mod mod) => base.IsValidMod(mod) && mod.ValidForMultiplayer; + + protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && mod.ValidForMultiplayerAsFreeMod; } } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 6a559dbb2c08..c4503773ad14 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -14,6 +14,7 @@ using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.Rooms; +using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -45,7 +46,6 @@ public abstract class OnlinePlaySongSelect : SongSelect, IOnlinePlaySubScreen protected readonly Bindable> FreeMods = new Bindable>(Array.Empty()); - private readonly FreeModSelectOverlay freeModSelectOverlay; private readonly Room room; private WorkingBeatmap initialBeatmap; @@ -53,13 +53,16 @@ public abstract class OnlinePlaySongSelect : SongSelect, IOnlinePlaySubScreen private IReadOnlyList initialMods; private bool itemSelected; + private readonly FreeModSelectScreen freeModSelectOverlay; + private IDisposable freeModSelectOverlayRegistration; + protected OnlinePlaySongSelect(Room room) { this.room = room; Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }; - freeModSelectOverlay = new FreeModSelectOverlay + freeModSelectOverlay = new FreeModSelectScreen { SelectedMods = { BindTarget = FreeMods }, IsValidMod = IsValidFreeMod, @@ -75,7 +78,7 @@ private void load() initialRuleset = Ruleset.Value; initialMods = Mods.Value.ToList(); - FooterPanels.Add(freeModSelectOverlay); + LoadComponent(freeModSelectOverlay); } protected override void LoadComplete() @@ -94,6 +97,8 @@ protected override void LoadComplete() Mods.BindValueChanged(onModsChanged); Ruleset.BindValueChanged(onRulesetChanged); + + freeModSelectOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(freeModSelectOverlay); } private void onModsChanged(ValueChangedEvent> mods) @@ -150,10 +155,12 @@ public override bool OnExiting(ScreenExitEvent e) Mods.Value = initialMods; } + freeModSelectOverlay.Hide(); + return base.OnExiting(e); } - protected override ModSelectOverlay CreateModSelectOverlay() => new UserModSelectOverlay + protected override ModSelectScreen CreateModSelectOverlay() => new UserModSelectScreen(OverlayColourScheme.Plum) { IsValidMod = IsValidMod }; @@ -182,5 +189,12 @@ public override bool OnExiting(ScreenExitEvent e) private bool checkCompatibleFreeMod(Mod mod) => Mods.Value.All(m => m.Acronym != mod.Acronym) // Mod must not be contained in the required mods. && ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray()); // Mod must be compatible with all the required mods. + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + freeModSelectOverlayRegistration?.Dispose(); + } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index ae3eb1ed8bd7..51c1e6b43b8d 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -16,6 +16,7 @@ using osu.Framework.Logging; using osu.Framework.Screens; using osu.Framework.Threading; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.Containers; @@ -37,7 +38,6 @@ namespace osu.Game.Screens.Play { [Cached] - [Cached(typeof(ISamplePlaybackDisabler))] public abstract class Player : ScreenWithBeatmapBackground, ISamplePlaybackDisabler, ILocalUserPlayInfo { /// diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index c2ef5529e882..a5341242e247 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -85,7 +85,6 @@ public ScorePanelList() InternalChild = scroll = new Scroll { RelativeSizeAxes = Axes.Both, - HandleScroll = () => expandedPanel?.IsHovered != true, // handle horizontal scroll only when not hovering the expanded panel. Child = flow = new Flow { Anchor = Anchor.Centre, @@ -359,11 +358,6 @@ public Scroll() /// public float? InstantScrollTarget; - /// - /// Whether this container should handle scroll trigger events. - /// - public Func HandleScroll; - protected override void UpdateAfterChildren() { if (InstantScrollTarget != null) @@ -374,10 +368,6 @@ protected override void UpdateAfterChildren() base.UpdateAfterChildren(); } - - public override bool HandlePositionalInput => HandleScroll(); - - public override bool HandleNonPositionalInput => HandleScroll(); } } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 2a1ed2a7a874..eb5e99697293 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -35,6 +35,7 @@ using osu.Game.Collections; using osu.Game.Graphics.UserInterface; using System.Diagnostics; +using JetBrains.Annotations; using osu.Game.Screens.Play; using osu.Game.Database; using osu.Game.Skinning; @@ -101,7 +102,7 @@ public abstract class SongSelect : ScreenWithBeatmapBackground, IKeyBindingHandl [Resolved(CanBeNull = true)] private LegacyImportManager legacyImportManager { get; set; } - protected ModSelectOverlay ModSelect { get; private set; } + protected ModSelectScreen ModSelect { get; private set; } protected Sample SampleConfirm { get; private set; } @@ -116,9 +117,15 @@ public abstract class SongSelect : ScreenWithBeatmapBackground, IKeyBindingHandl private double audioFeedbackLastPlaybackTime; + [CanBeNull] + private IDisposable modSelectOverlayRegistration; + [Resolved] private MusicController music { get; set; } + [Resolved(CanBeNull = true)] + internal IOverlayManager OverlayManager { get; private set; } + [BackgroundDependencyLoader(true)] private void load(AudioManager audio, IDialogOverlay dialog, OsuColour colours, ManageCollectionsDialog manageCollectionsDialog, DifficultyRecommender recommender) { @@ -252,38 +259,25 @@ private void load(AudioManager audio, IDialogOverlay dialog, OsuColour colours, { AddRangeInternal(new Drawable[] { - new GridContainer // used for max height implementation + FooterPanels = new Container { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, RelativeSizeAxes = Axes.Both, - RowDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.Relative, 1f, maxSize: ModSelectOverlay.HEIGHT + Footer.HEIGHT), - }, - Content = new[] + Padding = new MarginPadding { Bottom = Footer.HEIGHT }, + Children = new Drawable[] { - null, - new Drawable[] - { - FooterPanels = new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Bottom = Footer.HEIGHT }, - Children = new Drawable[] - { - BeatmapOptions = new BeatmapOptionsOverlay(), - ModSelect = CreateModSelectOverlay() - } - } - } + BeatmapOptions = new BeatmapOptionsOverlay(), } }, - Footer = new Footer() + Footer = new Footer(), }); } + // preload the mod select overlay for later use in `LoadComplete()`. + // therein it will be registered at the `OsuGame` level to properly function as a blocking overlay. + LoadComponent(ModSelect = CreateModSelectOverlay()); + if (Footer != null) { foreach (var (button, overlay) in CreateFooterButtons()) @@ -317,6 +311,13 @@ private void load(AudioManager audio, IDialogOverlay dialog, OsuColour colours, } } + protected override void LoadComplete() + { + base.LoadComplete(); + + modSelectOverlayRegistration = OverlayManager?.RegisterBlockingOverlay(ModSelect); + } + /// /// Creates the buttons to be displayed in the footer. /// @@ -332,7 +333,7 @@ private void load(AudioManager audio, IDialogOverlay dialog, OsuColour colours, (new FooterButtonOptions(), BeatmapOptions) }; - protected virtual ModSelectOverlay CreateModSelectOverlay() => new UserModSelectOverlay(); + protected virtual ModSelectScreen CreateModSelectOverlay() => new UserModSelectScreen(); protected virtual void ApplyFilterToCarousel(FilterCriteria criteria) { @@ -658,6 +659,7 @@ public override bool OnExiting(ScreenExitEvent e) return true; beatmapInfoWedge.Hide(); + ModSelect.Hide(); this.FadeOut(100); @@ -716,6 +718,8 @@ protected override void Dispose(bool isDisposing) if (music != null) music.TrackChanged -= ensureTrackLooping; + + modSelectOverlayRegistration?.Dispose(); } /// diff --git a/osu.Game/Skinning/PausableSkinnableSound.cs b/osu.Game/Skinning/PausableSkinnableSound.cs index 10b8c47028d7..b34351d4e7f3 100644 --- a/osu.Game/Skinning/PausableSkinnableSound.cs +++ b/osu.Game/Skinning/PausableSkinnableSound.cs @@ -8,7 +8,6 @@ using osu.Framework.Bindables; using osu.Framework.Threading; using osu.Game.Audio; -using osu.Game.Screens.Play; namespace osu.Game.Skinning { diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index e9069d8c9c05..0fa2f3e78621 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -1,12 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Framework.Testing; +using osu.Game.Graphics; using osu.Game.Overlays; using osu.Game.Screens; @@ -15,11 +18,12 @@ namespace osu.Game.Tests.Visual /// /// A test case which can be used to test a screen (that relies on OnEntering being called to execute startup instructions). /// - public abstract class ScreenTestScene : OsuManualInputManagerTestScene + public abstract class ScreenTestScene : OsuManualInputManagerTestScene, IOverlayManager { protected readonly OsuScreenStack Stack; private readonly Container content; + private readonly Container overlayContent; protected override Container Content => content; @@ -36,7 +40,11 @@ protected ScreenTestScene() RelativeSizeAxes = Axes.Both }, content = new Container { RelativeSizeAxes = Axes.Both }, - DialogOverlay = new DialogOverlay() + overlayContent = new Container + { + RelativeSizeAxes = Axes.Both, + Child = DialogOverlay = new DialogOverlay() + } }); Stack.ScreenPushed += (lastScreen, newScreen) => Logger.Log($"{nameof(ScreenTestScene)} screen changed → {newScreen}"); @@ -65,5 +73,26 @@ private void addExitAllScreensStep() return false; }); } + + #region IOverlayManager + + IBindable IOverlayManager.OverlayActivationMode { get; } = new Bindable(OverlayActivation.All); + + // in the blocking methods below it is important to be careful about threading (e.g. use `Expire()` rather than `Remove()`, and schedule transforms), + // because in the worst case the clean-up methods could be called from async disposal. + + IDisposable IOverlayManager.RegisterBlockingOverlay(OverlayContainer overlayContainer) + { + overlayContent.Add(overlayContainer); + return new InvokeOnDisposal(() => overlayContainer.Expire()); + } + + void IOverlayManager.ShowBlockingOverlay(OverlayContainer overlay) + => Schedule(() => Stack.FadeColour(OsuColour.Gray(0.5f), 500, Easing.OutQuint)); + + void IOverlayManager.HideBlockingOverlay(OverlayContainer overlay) + => Schedule(() => Stack.FadeColour(Colour4.White, 500, Easing.OutQuint)); + + #endregion } } diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 8df44216b6c1..ea092a8ca3a5 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -106,22 +106,69 @@ public static bool CheckAllowed(IEnumerable combination, IEnumerable } /// - /// Check the provided combination of mods are valid for a local gameplay session. + /// Checks that all s in a combination are valid for a local gameplay session. /// /// The mods to check. - /// Invalid mods, if any were found. Can be null if all mods were valid. + /// Invalid mods, if any were found. Will be null if all mods were valid. /// Whether the input mods were all valid. If false, will contain all invalid entries. public static bool CheckValidForGameplay(IEnumerable mods, [NotNullWhen(false)] out List? invalidMods) { mods = mods.ToArray(); - // exclude multi mods from compatibility checks. - // the loop below automatically marks all multi mods as not valid for gameplay anyway. - CheckCompatibleSet(mods.Where(m => !(m is MultiMod)), out invalidMods); + // checking compatibility of multi mods would try to flatten them and return incompatible mods. + // in gameplay context, we never want MultiMod selected in the first place, therefore check against it first. + if (!checkValid(mods, m => !(m is MultiMod), out invalidMods)) + return false; + + if (!CheckCompatibleSet(mods, out invalidMods)) + return false; + + return checkValid(mods, m => m.Type != ModType.System && m.HasImplementation, out invalidMods); + } + + /// + /// Checks that all s in a combination are valid as "required mods" in a multiplayer match session. + /// + /// The mods to check. + /// Invalid mods, if any were found. Will be null if all mods were valid. + /// Whether the input mods were all valid. If false, will contain all invalid entries. + public static bool CheckValidRequiredModsForMultiplayer(IEnumerable mods, [NotNullWhen(false)] out List? invalidMods) + { + mods = mods.ToArray(); + + // checking compatibility of multi mods would try to flatten them and return incompatible mods. + // in gameplay context, we never want MultiMod selected in the first place, therefore check against it first. + if (!checkValid(mods, m => !(m is MultiMod), out invalidMods)) + return false; + + if (!CheckCompatibleSet(mods, out invalidMods)) + return false; + + return checkValid(mods, m => m.Type != ModType.System && m.HasImplementation && m.ValidForMultiplayer, out invalidMods); + } + + /// + /// Checks that all s in a combination are valid as "free mods" in a multiplayer match session. + /// + /// + /// Note that this does not check compatibility between mods, + /// given that the passed mods are expected to be the ones to be allowed for the multiplayer match, + /// not to be confused with the list of mods the user currently has selected for the multiplayer match. + /// + /// The mods to check. + /// Invalid mods, if any were found. Will be null if all mods were valid. + /// Whether the input mods were all valid. If false, will contain all invalid entries. + public static bool CheckValidFreeModsForMultiplayer(IEnumerable mods, [NotNullWhen(false)] out List? invalidMods) + => checkValid(mods, m => m.Type != ModType.System && m.HasImplementation && m.ValidForMultiplayerAsFreeMod && !(m is MultiMod), out invalidMods); + + private static bool checkValid(IEnumerable mods, Predicate valid, [NotNullWhen(false)] out List? invalidMods) + { + mods = mods.ToArray(); + invalidMods = null; foreach (var mod in mods) { - if (mod.Type == ModType.System || !mod.HasImplementation || mod is MultiMod) + if (!valid(mod)) { invalidMods ??= new List(); invalidMods.Add(mod); diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs index dbf04283b68b..d9c8199f75fd 100644 --- a/osu.Game/Utils/SentryLogger.cs +++ b/osu.Game/Utils/SentryLogger.cs @@ -24,7 +24,7 @@ public SentryLogger(OsuGame game) var options = new SentryOptions { - Dsn = "https://5e342cd55f294edebdc9ad604d28bbd3@sentry.io/1255255", + Dsn = "https://ad9f78529cef40ac874afb95a9aca04e@sentry.ppy.sh/2", Release = game.Version }; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index fa7563da557b..2f32c843c00f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -35,7 +35,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index e472b5f1a819..b4832676960e 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} @@ -61,7 +61,7 @@ - + @@ -84,7 +84,7 @@ - + diff --git a/osu.sln b/osu.sln index b5018db3624a..aeec0843be89 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