diff --git a/README.md b/README.md index cf7ce35791..792e2d646a 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Please make sure you have the following prerequisites: - A desktop platform with the [.NET 6.0 SDK](https://dotnet.microsoft.com/download) installed. -When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as the latest version of [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/). +When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as the latest version of [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/), or [Visual Studio Code](https://code.visualstudio.com/) with the [EditorConfig](https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig) and [C#](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp) plugin installed. ### Downloading the source code diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs index 3c32b4fa65..3b051912fd 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleLateFade.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Scoring; using osu.Game.Tests.Visual; using osuTK; @@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Tests private float? alphaAtMiss; [Test] - public void TestHitCircleClassicMod() + public void TestHitCircleClassicModMiss() { AddStep("Create hit circle", () => { @@ -61,8 +62,27 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("Transparent when missed", () => alphaAtMiss == 0); } + /// + /// No early fade is expected to be applied if the hit circle has been hit. + /// [Test] - public void TestHitCircleNoMod() + public void TestHitCircleClassicModHit() + { + TestDrawableHitCircle circle = null!; + + AddStep("Create hit circle", () => + { + SelectedMods.Value = new Mod[] { new OsuModClassic() }; + circle = createCircle(true); + }); + + AddUntilStep("Wait until circle is hit", () => circle.Result?.Type == HitResult.Great); + AddUntilStep("Wait for miss window", () => Clock.CurrentTime, () => Is.GreaterThanOrEqualTo(circle.HitObject.StartTime + circle.HitObject.HitWindows.WindowFor(HitResult.Miss))); + AddAssert("Check circle is still visible", () => circle.Alpha, () => Is.GreaterThan(0)); + } + + [Test] + public void TestHitCircleNoModMiss() { AddStep("Create hit circle", () => { @@ -74,6 +94,16 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("Opaque when missed", () => alphaAtMiss == 1); } + [Test] + public void TestHitCircleNoModHit() + { + AddStep("Create hit circle", () => + { + SelectedMods.Value = Array.Empty(); + createCircle(true); + }); + } + [Test] public void TestSliderClassicMod() { @@ -100,27 +130,32 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("Head circle opaque when missed", () => alphaAtMiss == 1); } - private void createCircle() + private TestDrawableHitCircle createCircle(bool shouldHit = false) { alphaAtMiss = null; - DrawableHitCircle drawableHitCircle = new DrawableHitCircle(new HitCircle + TestDrawableHitCircle drawableHitCircle = new TestDrawableHitCircle(new HitCircle { StartTime = Time.Current + 500, - Position = new Vector2(250) - }); + Position = new Vector2(250), + }, shouldHit); + + drawableHitCircle.Scale = new Vector2(2f); foreach (var mod in SelectedMods.Value.OfType()) mod.ApplyToDrawableHitObject(drawableHitCircle); drawableHitCircle.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - drawableHitCircle.OnNewResult += (_, _) => + drawableHitCircle.OnNewResult += (_, result) => { - alphaAtMiss = drawableHitCircle.Alpha; + if (!result.IsHit) + alphaAtMiss = drawableHitCircle.Alpha; }; Child = drawableHitCircle; + + return drawableHitCircle; } private void createSlider() @@ -138,6 +173,8 @@ namespace osu.Game.Rulesets.Osu.Tests }) }); + drawableSlider.Scale = new Vector2(2f); + drawableSlider.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); drawableSlider.OnLoadComplete += _ => @@ -145,12 +182,36 @@ namespace osu.Game.Rulesets.Osu.Tests foreach (var mod in SelectedMods.Value.OfType()) mod.ApplyToDrawableHitObject(drawableSlider.HeadCircle); - drawableSlider.HeadCircle.OnNewResult += (_, _) => + drawableSlider.HeadCircle.OnNewResult += (_, result) => { - alphaAtMiss = drawableSlider.HeadCircle.Alpha; + if (!result.IsHit) + alphaAtMiss = drawableSlider.HeadCircle.Alpha; }; }; + Child = drawableSlider; } + + protected partial class TestDrawableHitCircle : DrawableHitCircle + { + private readonly bool shouldHit; + + public TestDrawableHitCircle(HitCircle h, bool shouldHit) + : base(h) + { + this.shouldHit = shouldHit; + } + + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + if (shouldHit && !userTriggered && timeOffset >= 0 && CheckHittable?.Invoke(this, Time.Current) != false) + { + // force success + ApplyResult(r => r.Type = HitResult.Great); + } + else + base.CheckForResult(userTriggered, timeOffset); + } + } } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 82deec41f5..5dbf23f7ea 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -89,13 +89,16 @@ namespace osu.Game.Rulesets.Osu.Mods private void applyEarlyFading(DrawableHitCircle circle) { - circle.ApplyCustomUpdateState += (o, _) => + circle.ApplyCustomUpdateState += (dho, state) => { - using (o.BeginAbsoluteSequence(o.StateUpdateTime)) + using (dho.BeginAbsoluteSequence(dho.StateUpdateTime)) { - double okWindow = o.HitObject.HitWindows.WindowFor(HitResult.Ok); - double lateMissFadeTime = o.HitObject.HitWindows.WindowFor(HitResult.Meh) - okWindow; - o.Delay(okWindow).FadeOut(lateMissFadeTime); + if (state != ArmedState.Hit) + { + double okWindow = dho.HitObject.HitWindows.WindowFor(HitResult.Ok); + double lateMissFadeTime = dho.HitObject.HitWindows.WindowFor(HitResult.Meh) - okWindow; + dho.Delay(okWindow).FadeOut(lateMissFadeTime); + } } }; } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs index 54ee1659e1..d0fa5fc737 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneBeatmapEditorNavigation.cs @@ -9,6 +9,7 @@ using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; @@ -16,6 +17,7 @@ using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.GameplayTest; using osu.Game.Screens.Menu; using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; using osu.Game.Tests.Resources; using osuTK.Input; @@ -203,6 +205,33 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for music stopped", () => !Game.MusicController.IsPlaying); } + [TestCase(SortMode.Title)] + [TestCase(SortMode.Difficulty)] + public void TestSelectionRetainedOnExit(SortMode sortMode) + { + BeatmapSetInfo beatmapSet = null!; + + AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely()); + AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach()); + + AddStep($"set sort mode to {sortMode}", () => Game.LocalConfig.SetValue(OsuSetting.SongSelectSortingMode, sortMode)); + AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet)); + AddUntilStep("wait for song select", + () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet) + && Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect + && songSelect.IsLoaded); + + AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0))); + AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse); + + AddStep("exit editor", () => InputManager.Key(Key.Escape)); + AddUntilStep("wait for editor exit", () => Game.ScreenStack.CurrentScreen is not Editor); + + AddUntilStep("selection retained on song select", + () => Game.Beatmap.Value.BeatmapInfo.ID, + () => Is.EqualTo(beatmapSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0).ID)); + } + private EditorBeatmap getEditorBeatmap() => getEditor().ChildrenOfType().Single(); private Editor getEditor() => (Editor)Game.ScreenStack.CurrentScreen; diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 61f95dc628..040b341584 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -39,6 +39,7 @@ namespace osu.Game.Tests.Visual.SongSelect private BeatmapInfo currentSelection => carousel.SelectedBeatmapInfo; private const int set_count = 5; + private const int diff_count = 3; [BackgroundDependencyLoader] private void load(RulesetStore rulesets) @@ -111,7 +112,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestScrollPositionMaintainedOnAdd() { - loadBeatmaps(count: 1, randomDifficulties: false); + loadBeatmaps(setCount: 1); for (int i = 0; i < 10; i++) { @@ -124,7 +125,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestDeletion() { - loadBeatmaps(count: 5, randomDifficulties: true); + loadBeatmaps(setCount: 5, randomDifficulties: true); AddStep("remove first set", () => carousel.RemoveBeatmapSet(carousel.Items.Select(item => item.Item).OfType().First().BeatmapSet)); AddUntilStep("4 beatmap sets visible", () => this.ChildrenOfType().Count(set => set.Alpha > 0) == 4); @@ -133,7 +134,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestScrollPositionMaintainedOnDelete() { - loadBeatmaps(count: 50, randomDifficulties: false); + loadBeatmaps(setCount: 50); for (int i = 0; i < 10; i++) { @@ -150,7 +151,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestManyPanels() { - loadBeatmaps(count: 5000, randomDifficulties: true); + loadBeatmaps(setCount: 5000, randomDifficulties: true); } [Test] @@ -501,6 +502,33 @@ namespace osu.Game.Tests.Visual.SongSelect waitForSelection(set_count); } + [Test] + public void TestAddRemoveDifficultySort() + { + const int local_set_count = 2; + const int local_diff_count = 2; + + loadBeatmaps(setCount: local_set_count, diffCount: local_diff_count); + + AddStep("Sort by difficulty", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }, false)); + + checkVisibleItemCount(false, local_set_count * local_diff_count); + + var firstAdded = TestResources.CreateTestBeatmapSetInfo(local_diff_count); + + AddStep("Add new set", () => carousel.UpdateBeatmapSet(firstAdded)); + + checkVisibleItemCount(false, (local_set_count + 1) * local_diff_count); + + AddStep("Remove set", () => carousel.RemoveBeatmapSet(firstAdded)); + + checkVisibleItemCount(false, (local_set_count) * local_diff_count); + + setSelected(local_set_count, 1); + + waitForSelection(local_set_count); + } + [Test] public void TestSelectionEnteringFromEmptyRuleset() { @@ -662,7 +690,7 @@ namespace osu.Game.Tests.Visual.SongSelect for (int i = 0; i < 3; i++) { - var set = TestResources.CreateTestBeatmapSetInfo(3); + var set = TestResources.CreateTestBeatmapSetInfo(diff_count); // only need to set the first as they are a shared reference. var beatmap = set.Beatmaps.First(); @@ -709,7 +737,7 @@ namespace osu.Game.Tests.Visual.SongSelect for (int i = 0; i < 3; i++) { - var set = TestResources.CreateTestBeatmapSetInfo(3); + var set = TestResources.CreateTestBeatmapSetInfo(diff_count); // only need to set the first as they are a shared reference. var beatmap = set.Beatmaps.First(); @@ -758,32 +786,54 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] - public void TestSortingWithFiltered() + public void TestSortingWithDifficultyFiltered() { + const int local_diff_count = 3; + const int local_set_count = 2; + List sets = new List(); AddStep("Populuate beatmap sets", () => { sets.Clear(); - for (int i = 0; i < 3; i++) + for (int i = 0; i < local_set_count; i++) { - var set = TestResources.CreateTestBeatmapSetInfo(3); + var set = TestResources.CreateTestBeatmapSetInfo(local_diff_count); set.Beatmaps[0].StarRating = 3 - i; - set.Beatmaps[2].StarRating = 6 + i; + set.Beatmaps[1].StarRating = 6 + i; sets.Add(set); } }); loadBeatmaps(sets); + AddStep("Sort by difficulty", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }, false)); + + checkVisibleItemCount(false, local_set_count * local_diff_count); + checkVisibleItemCount(true, 1); + AddStep("Filter to normal", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Normal" }, false)); - AddAssert("Check first set at end", () => carousel.BeatmapSets.First().Equals(sets.Last())); - AddAssert("Check last set at start", () => carousel.BeatmapSets.Last().Equals(sets.First())); + checkVisibleItemCount(false, local_set_count); + checkVisibleItemCount(true, 1); + + AddUntilStep("Check all visible sets have one normal", () => + { + return carousel.Items.OfType() + .Where(p => p.IsPresent) + .Count(p => ((CarouselBeatmapSet)p.Item)!.Beatmaps.Single().BeatmapInfo.DifficultyName.StartsWith("Normal", StringComparison.Ordinal)) == local_set_count; + }); AddStep("Filter to insane", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty, SearchText = "Insane" }, false)); - AddAssert("Check first set at start", () => carousel.BeatmapSets.First().Equals(sets.First())); - AddAssert("Check last set at end", () => carousel.BeatmapSets.Last().Equals(sets.Last())); + checkVisibleItemCount(false, local_set_count); + checkVisibleItemCount(true, 1); + + AddUntilStep("Check all visible sets have one insane", () => + { + return carousel.Items.OfType() + .Where(p => p.IsPresent) + .Count(p => ((CarouselBeatmapSet)p.Item)!.Beatmaps.Single().BeatmapInfo.DifficultyName.StartsWith("Insane", StringComparison.Ordinal)) == local_set_count; + }); } [Test] @@ -838,7 +888,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("create hidden set", () => { - hidingSet = TestResources.CreateTestBeatmapSetInfo(3); + hidingSet = TestResources.CreateTestBeatmapSetInfo(diff_count); hidingSet.Beatmaps[1].Hidden = true; hiddenList.Clear(); @@ -885,7 +935,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("add mixed ruleset beatmapset", () => { - testMixed = TestResources.CreateTestBeatmapSetInfo(3); + testMixed = TestResources.CreateTestBeatmapSetInfo(diff_count); for (int i = 0; i <= 2; i++) { @@ -907,7 +957,7 @@ namespace osu.Game.Tests.Visual.SongSelect BeatmapSetInfo testSingle = null; AddStep("add single ruleset beatmapset", () => { - testSingle = TestResources.CreateTestBeatmapSetInfo(3); + testSingle = TestResources.CreateTestBeatmapSetInfo(diff_count); testSingle.Beatmaps.ForEach(b => { b.Ruleset = rulesets.AvailableRulesets.ElementAt(1); @@ -930,7 +980,7 @@ namespace osu.Game.Tests.Visual.SongSelect manySets.Clear(); for (int i = 1; i <= 50; i++) - manySets.Add(TestResources.CreateTestBeatmapSetInfo(3)); + manySets.Add(TestResources.CreateTestBeatmapSetInfo(diff_count)); }); loadBeatmaps(manySets); @@ -955,6 +1005,43 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1); } + [Test] + public void TestCarouselRemembersSelectionDifficultySort() + { + List manySets = new List(); + + AddStep("Populate beatmap sets", () => + { + manySets.Clear(); + + for (int i = 1; i <= 50; i++) + manySets.Add(TestResources.CreateTestBeatmapSetInfo(diff_count)); + }); + + loadBeatmaps(manySets); + + AddStep("Sort by difficulty", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty }, false)); + + advanceSelection(direction: 1, diff: false); + + for (int i = 0; i < 5; i++) + { + AddStep("Toggle non-matching filter", () => + { + carousel.Filter(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }, false); + }); + + AddStep("Restore no filter", () => + { + carousel.Filter(new FilterCriteria(), false); + eagerSelectedIDs.Add(carousel.SelectedBeatmapSet!.ID); + }); + } + + // always returns to same selection as long as it's available. + AddAssert("Selection was remembered", () => eagerSelectedIDs.Count == 1); + } + [Test] public void TestFilteringByUserStarDifficulty() { @@ -1081,8 +1168,8 @@ namespace osu.Game.Tests.Visual.SongSelect } } - private void loadBeatmaps(List beatmapSets = null, Func initialCriteria = null, Action carouselAdjust = null, int? count = null, - bool randomDifficulties = false) + private void loadBeatmaps(List beatmapSets = null, Func initialCriteria = null, Action carouselAdjust = null, + int? setCount = null, int? diffCount = null, bool randomDifficulties = false) { bool changed = false; @@ -1090,11 +1177,11 @@ namespace osu.Game.Tests.Visual.SongSelect { beatmapSets = new List(); - for (int i = 1; i <= (count ?? set_count); i++) + for (int i = 1; i <= (setCount ?? set_count); i++) { beatmapSets.Add(randomDifficulties ? TestResources.CreateTestBeatmapSetInfo() - : TestResources.CreateTestBeatmapSetInfo(3)); + : TestResources.CreateTestBeatmapSetInfo(diffCount ?? diff_count)); } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs index 11d55bc0bd..6d97be730b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneUpdateBeatmapSetButton.cs @@ -15,6 +15,7 @@ using osu.Game.Overlays; using osu.Game.Overlays.Dialog; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Carousel; +using osu.Game.Screens.Select.Filter; using osu.Game.Tests.Online; using osu.Game.Tests.Resources; using osuTK.Input; @@ -192,6 +193,57 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("release mouse button", () => InputManager.ReleaseButton(MouseButton.Left)); } + [Test] + public void TestSplitDisplay() + { + ArchiveDownloadRequest? downloadRequest = null; + + AddStep("set difficulty sort mode", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Difficulty })); + AddStep("update online hash", () => + { + testBeatmapSetInfo.Beatmaps.First().OnlineMD5Hash = "different hash"; + testBeatmapSetInfo.Beatmaps.First().LastOnlineUpdate = DateTimeOffset.Now; + + carousel.UpdateBeatmapSet(testBeatmapSetInfo); + }); + + AddUntilStep("multiple \"sets\" visible", () => carousel.ChildrenOfType().Count(), () => Is.GreaterThan(1)); + AddUntilStep("update button visible", getUpdateButton, () => Is.Not.Null); + + AddStep("click button", () => getUpdateButton()?.TriggerClick()); + + AddUntilStep("wait for download started", () => + { + downloadRequest = beatmapDownloader.GetExistingDownload(testBeatmapSetInfo); + return downloadRequest != null; + }); + + AddUntilStep("wait for button disabled", () => getUpdateButton()?.Enabled.Value == false); + + AddUntilStep("progress download to completion", () => + { + if (downloadRequest is TestSceneOnlinePlayBeatmapAvailabilityTracker.TestDownloadRequest testRequest) + { + testRequest.SetProgress(testRequest.Progress + 0.1f); + + if (testRequest.Progress >= 1) + { + testRequest.TriggerSuccess(); + + // usually this would be done by the import process. + testBeatmapSetInfo.Beatmaps.First().MD5Hash = "different hash"; + testBeatmapSetInfo.Beatmaps.First().LastOnlineUpdate = DateTimeOffset.Now; + + // usually this would be done by a realm subscription. + carousel.UpdateBeatmapSet(testBeatmapSetInfo); + return true; + } + } + + return false; + }); + } + private BeatmapCarousel createCarousel() { return carousel = new BeatmapCarousel @@ -199,7 +251,7 @@ namespace osu.Game.Tests.Visual.SongSelect RelativeSizeAxes = Axes.Both, BeatmapSets = new List { - (testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo()), + (testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(5)), } }; } diff --git a/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs b/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs index e0444b6126..95d6b6d107 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneSongBar.cs @@ -58,9 +58,14 @@ namespace osu.Game.Tournament.Tests.Components songBar.Beatmap = new TournamentBeatmap(beatmap); }); + AddStep("set mods to HR", () => songBar.Mods = LegacyMods.HardRock); AddStep("set mods to DT", () => songBar.Mods = LegacyMods.DoubleTime); AddStep("unset mods", () => songBar.Mods = LegacyMods.None); + + AddToggleStep("toggle expanded", expanded => songBar.Expanded = expanded); + + AddStep("set null beatmap", () => songBar.Beatmap = null); } } } diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs index f3c3fdec97..a58f09d13a 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs @@ -12,6 +12,13 @@ namespace osu.Game.Tournament.Tests.Screens { public partial class TestSceneScheduleScreen : TournamentScreenTestScene { + public override void SetUpSteps() + { + AddStep("clear matches", () => Ladder.Matches.Clear()); + + base.SetUpSteps(); + } + [BackgroundDependencyLoader] private void load() { @@ -34,6 +41,36 @@ namespace osu.Game.Tournament.Tests.Screens AddStep("Set null current match", () => Ladder.CurrentMatch.Value = null); } + [Test] + public void TestUpcomingMatches() + { + AddStep("Add upcoming match", () => + { + var tournamentMatch = CreateSampleMatch(); + + tournamentMatch.Date.Value = DateTimeOffset.UtcNow.AddMinutes(5); + tournamentMatch.Completed.Value = false; + + Ladder.Matches.Add(tournamentMatch); + }); + } + + [Test] + public void TestRecentMatches() + { + AddStep("Add recent match", () => + { + var tournamentMatch = CreateSampleMatch(); + + tournamentMatch.Date.Value = DateTimeOffset.UtcNow; + tournamentMatch.Completed.Value = true; + tournamentMatch.Team1Score.Value = tournamentMatch.PointsToWin; + tournamentMatch.Team2Score.Value = tournamentMatch.PointsToWin / 2; + + Ladder.Matches.Add(tournamentMatch); + }); + } + private void setMatchDate(TimeSpan relativeTime) // Humanizer cannot handle negative timespans. => AddStep($"start time is {relativeTime}", () => diff --git a/osu.Game.Tournament.Tests/TournamentScreenTestScene.cs b/osu.Game.Tournament.Tests/TournamentScreenTestScene.cs index 8adffe1468..e8cca00c92 100644 --- a/osu.Game.Tournament.Tests/TournamentScreenTestScene.cs +++ b/osu.Game.Tournament.Tests/TournamentScreenTestScene.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tournament.Tests { public TournamentScalingContainer() { - TargetDrawSize = new Vector2(1920, 1080); + TargetDrawSize = new Vector2(1024, 768); RelativeSizeAxes = Axes.Both; } diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index bf835122cc..5407c21079 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -92,8 +92,16 @@ namespace osu.Game.Tournament.IPC else { beatmapLookupRequest = new GetBeatmapRequest(new APIBeatmap { OnlineID = beatmapId }); - beatmapLookupRequest.Success += b => Beatmap.Value = new TournamentBeatmap(b); - beatmapLookupRequest.Failure += _ => Beatmap.Value = null; + beatmapLookupRequest.Success += b => + { + if (lastBeatmapId == beatmapId) + Beatmap.Value = new TournamentBeatmap(b); + }; + beatmapLookupRequest.Failure += _ => + { + if (lastBeatmapId == beatmapId) + Beatmap.Value = null; + }; API.Queue(beatmapLookupRequest); } } diff --git a/osu.Game.Tournament/SaveChangesOverlay.cs b/osu.Game.Tournament/SaveChangesOverlay.cs index 7838d4ba48..0b5194a51f 100644 --- a/osu.Game.Tournament/SaveChangesOverlay.cs +++ b/osu.Game.Tournament/SaveChangesOverlay.cs @@ -7,13 +7,16 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Online.Multiplayer; using osuTK; namespace osu.Game.Tournament { - internal partial class SaveChangesOverlay : CompositeDrawable + internal partial class SaveChangesOverlay : CompositeDrawable, IKeyBindingHandler { [Resolved] private TournamentGame tournamentGame { get; set; } = null!; @@ -78,6 +81,21 @@ namespace osu.Game.Tournament scheduleNextCheck(); } + public bool OnPressed(KeyBindingPressEvent e) + { + if (e.Action == PlatformAction.Save && !e.Repeat) + { + saveChangesButton.TriggerClick(); + return true; + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + private void scheduleNextCheck() => Scheduler.AddDelayed(() => checkForChanges().FireAndForget(), 1000); private void saveChanges() diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs index 063c231add..d02559d6b7 100644 --- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs +++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.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.Bindables; @@ -19,6 +20,7 @@ namespace osu.Game.Tournament.Screens.Schedule { public partial class ScheduleScreen : TournamentScreen { + private readonly BindableList allMatches = new BindableList(); private readonly Bindable currentMatch = new Bindable(); private Container mainContainer = null!; private LadderInfo ladder = null!; @@ -101,19 +103,34 @@ namespace osu.Game.Tournament.Screens.Schedule { base.LoadComplete(); + allMatches.BindTo(ladder.Matches); + allMatches.BindCollectionChanged((_, _) => refresh()); + currentMatch.BindTo(ladder.CurrentMatch); - currentMatch.BindValueChanged(matchChanged, true); + currentMatch.BindValueChanged(_ => refresh(), true); } - private void matchChanged(ValueChangedEvent match) + private void refresh() { - var upcoming = ladder.Matches.Where(p => !p.Completed.Value && p.Team1.Value != null && p.Team2.Value != null && Math.Abs(p.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < 4); - var conditionals = ladder - .Matches.Where(p => !p.Completed.Value && (p.Team1.Value == null || p.Team2.Value == null) && Math.Abs(p.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < 4) - .SelectMany(m => m.ConditionalMatches.Where(cp => m.Acronyms.TrueForAll(a => cp.Acronyms.Contains(a)))); + const int days_for_displays = 4; - upcoming = upcoming.Concat(conditionals); - upcoming = upcoming.OrderBy(p => p.Date.Value).Take(8); + IEnumerable conditionals = + allMatches + .Where(m => !m.Completed.Value && (m.Team1.Value == null || m.Team2.Value == null) && Math.Abs(m.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < days_for_displays) + .SelectMany(m => m.ConditionalMatches.Where(cp => m.Acronyms.TrueForAll(a => cp.Acronyms.Contains(a)))); + + IEnumerable upcoming = + allMatches + .Where(m => !m.Completed.Value && m.Team1.Value != null && m.Team2.Value != null && Math.Abs(m.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < days_for_displays) + .Concat(conditionals) + .OrderBy(m => m.Date.Value) + .Take(8); + + var recent = + allMatches + .Where(m => m.Completed.Value && m.Team1.Value != null && m.Team2.Value != null && Math.Abs(m.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < days_for_displays) + .OrderByDescending(m => m.Date.Value) + .Take(8); ScheduleContainer comingUpNext; @@ -137,12 +154,7 @@ namespace osu.Game.Tournament.Screens.Schedule { RelativeSizeAxes = Axes.Both, Width = 0.4f, - ChildrenEnumerable = ladder.Matches - .Where(p => p.Completed.Value && p.Team1.Value != null && p.Team2.Value != null - && Math.Abs(p.Date.Value.DayOfYear - DateTimeOffset.UtcNow.DayOfYear) < 4) - .OrderByDescending(p => p.Date.Value) - .Take(8) - .Select(p => new ScheduleMatch(p)) + ChildrenEnumerable = recent.Select(p => new ScheduleMatch(p)) }, new ScheduleContainer("upcoming matches") { @@ -161,7 +173,7 @@ namespace osu.Game.Tournament.Screens.Schedule } }; - if (match.NewValue != null) + if (currentMatch.Value != null) { comingUpNext.Child = new FillFlowContainer { @@ -170,12 +182,12 @@ namespace osu.Game.Tournament.Screens.Schedule Spacing = new Vector2(30), Children = new Drawable[] { - new ScheduleMatch(match.NewValue, false) + new ScheduleMatch(currentMatch.Value, false) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, - new TournamentSpriteTextWithBackground(match.NewValue.Round.Value?.Name.Value ?? string.Empty) + new TournamentSpriteTextWithBackground(currentMatch.Value.Round.Value?.Name.Value ?? string.Empty) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -185,7 +197,7 @@ namespace osu.Game.Tournament.Screens.Schedule { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Text = match.NewValue.Team1.Value?.FullName + " vs " + match.NewValue.Team2.Value?.FullName, + Text = currentMatch.Value.Team1.Value?.FullName + " vs " + currentMatch.Value.Team2.Value?.FullName, Font = OsuFont.Torus.With(size: 24, weight: FontWeight.SemiBold) }, new FillFlowContainer @@ -196,7 +208,7 @@ namespace osu.Game.Tournament.Screens.Schedule Origin = Anchor.CentreLeft, Children = new Drawable[] { - new ScheduleMatchDate(match.NewValue.Date.Value) + new ScheduleMatchDate(currentMatch.Value.Date.Value) { Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular) } @@ -282,6 +294,7 @@ namespace osu.Game.Tournament.Screens.Schedule { Direction = FillDirection.Vertical, RelativeSizeAxes = Axes.Both, + Spacing = new Vector2(0, -6), Margin = new MarginPadding(10) }, } diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs index ba3b17b513..25dc8ae1e5 100644 --- a/osu.Game.Tournament/TournamentGame.cs +++ b/osu.Game.Tournament/TournamentGame.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.Drawing; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -48,8 +47,6 @@ namespace osu.Game.Tournament { frameworkConfig.BindWith(FrameworkSetting.WindowedSize, windowSize); - windowSize.MinValue = new Size(TournamentSceneManager.REQUIRED_WIDTH, TournamentSceneManager.STREAM_AREA_HEIGHT); - windowMode = frameworkConfig.GetBindable(FrameworkSetting.WindowMode); Add(loadingSpinner = new LoadingSpinner(true, true) diff --git a/osu.Game/Collections/ManageCollectionsDialog.cs b/osu.Game/Collections/ManageCollectionsDialog.cs index 31016b807b..cc0f23d030 100644 --- a/osu.Game/Collections/ManageCollectionsDialog.cs +++ b/osu.Game/Collections/ManageCollectionsDialog.cs @@ -23,6 +23,9 @@ namespace osu.Game.Collections private AudioFilter lowPassFilter = null!; + protected override string PopInSampleName => @"UI/overlay-big-pop-in"; + protected override string PopOutSampleName => @"UI/overlay-big-pop-out"; + public ManageCollectionsDialog() { Anchor = Anchor.Centre; diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index f92cfc2306..162c4b6a59 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -24,6 +24,7 @@ namespace osu.Game.Graphics.Containers private Sample samplePopOut; protected virtual string PopInSampleName => "UI/overlay-pop-in"; protected virtual string PopOutSampleName => "UI/overlay-pop-out"; + protected virtual double PopInOutSampleBalance => 0; protected override bool BlockNonPositionalInput => true; @@ -133,15 +134,21 @@ namespace osu.Game.Graphics.Containers return; } - if (didChange) - samplePopIn?.Play(); + if (didChange && samplePopIn != null) + { + samplePopIn.Balance.Value = PopInOutSampleBalance; + samplePopIn.Play(); + } if (BlockScreenWideMouse && DimMainContent) overlayManager?.ShowBlockingOverlay(this); break; case Visibility.Hidden: - if (didChange) - samplePopOut?.Play(); + if (didChange && samplePopOut != null) + { + samplePopOut.Balance.Value = PopInOutSampleBalance; + samplePopOut.Play(); + } if (BlockScreenWideMouse) overlayManager?.HideBlockingOverlay(this); break; diff --git a/osu.Game/Graphics/Containers/WaveContainer.cs b/osu.Game/Graphics/Containers/WaveContainer.cs index 9fd3d103c9..e84cb276a4 100644 --- a/osu.Game/Graphics/Containers/WaveContainer.cs +++ b/osu.Game/Graphics/Containers/WaveContainer.cs @@ -2,6 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -32,6 +35,12 @@ namespace osu.Game.Graphics.Containers protected override bool StartHidden => true; + private Sample? samplePopIn; + private Sample? samplePopOut; + + // required due to LoadAsyncComplete() in `VisibilityContainer` calling PopOut() during load - similar workaround to `OsuDropdownMenu` + private bool wasShown; + public Color4 FirstWaveColour { get => firstWave.Colour; @@ -56,6 +65,13 @@ namespace osu.Game.Graphics.Containers set => fourthWave.Colour = value; } + [BackgroundDependencyLoader(true)] + private void load(AudioManager audio) + { + samplePopIn = audio.Samples.Get("UI/wave-pop-in"); + samplePopOut = audio.Samples.Get("UI/overlay-big-pop-out"); + } + public WaveContainer() { Masking = true; @@ -110,6 +126,8 @@ namespace osu.Game.Graphics.Containers w.Show(); contentContainer.MoveToY(0, APPEAR_DURATION, Easing.OutQuint); + samplePopIn?.Play(); + wasShown = true; } protected override void PopOut() @@ -118,6 +136,9 @@ namespace osu.Game.Graphics.Containers w.Hide(); contentContainer.MoveToY(2, DISAPPEAR_DURATION, Easing.In); + + if (wasShown) + samplePopOut?.Play(); } protected override void UpdateAfterChildren() diff --git a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs index de4df96942..0eec04541c 100644 --- a/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs @@ -46,8 +46,8 @@ namespace osu.Game.Graphics.UserInterface private readonly Container content; private readonly Box hover; - public OsuAnimatedButton() - : base(HoverSampleSet.Button) + public OsuAnimatedButton(HoverSampleSet sampleSet = HoverSampleSet.Button) + : base(sampleSet) { base.Content.Add(content = new Container { diff --git a/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs b/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs index d5e0abe9d8..05ed531d02 100644 --- a/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs +++ b/osu.Game/Graphics/UserInterface/ShearedToggleButton.cs @@ -14,6 +14,12 @@ namespace osu.Game.Graphics.UserInterface private Sample? sampleOff; private Sample? sampleOn; + /// + /// Sheared toggle buttons by default play two samples when toggled: a click and a toggle (on/off). + /// Sometimes this might be too much. Setting this to false will silence the toggle sound. + /// + protected virtual bool PlayToggleSamples => true; + /// /// Whether this button is currently toggled to an active state. /// @@ -68,10 +74,13 @@ namespace osu.Game.Graphics.UserInterface { sampleClick?.Play(); - if (Active.Value) - sampleOn?.Play(); - else - sampleOff?.Play(); + if (PlayToggleSamples) + { + if (Active.Value) + sampleOn?.Play(); + else + sampleOff?.Play(); + } } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs b/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs index 381193d539..9b4689958c 100644 --- a/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs +++ b/osu.Game/Graphics/UserInterfaceV2/OsuPopover.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -21,6 +23,14 @@ namespace osu.Game.Graphics.UserInterfaceV2 private const float fade_duration = 250; private const double scale_duration = 500; + private Sample? samplePopIn; + private Sample? samplePopOut; + protected virtual string PopInSampleName => "UI/overlay-pop-in"; + protected virtual string PopOutSampleName => "UI/overlay-pop-out"; + + // required due to LoadAsyncComplete() in `VisibilityContainer` calling PopOut() during load - similar workaround to `OsuDropdownMenu` + private bool wasOpened; + public OsuPopover(bool withPadding = true) { Content.Padding = withPadding ? new MarginPadding(20) : new MarginPadding(); @@ -38,9 +48,11 @@ namespace osu.Game.Graphics.UserInterfaceV2 } [BackgroundDependencyLoader(true)] - private void load(OverlayColourProvider? colourProvider, OsuColour colours) + private void load(OverlayColourProvider? colourProvider, OsuColour colours, AudioManager audio) { Background.Colour = Arrow.Colour = colourProvider?.Background4 ?? colours.GreySeaFoamDarker; + samplePopIn = audio.Samples.Get(PopInSampleName); + samplePopOut = audio.Samples.Get(PopOutSampleName); } protected override Drawable CreateArrow() => Empty(); @@ -49,12 +61,18 @@ namespace osu.Game.Graphics.UserInterfaceV2 { this.ScaleTo(1, scale_duration, Easing.OutElasticHalf); this.FadeIn(fade_duration, Easing.OutQuint); + + samplePopIn?.Play(); + wasOpened = true; } protected override void PopOut() { this.ScaleTo(0.7f, scale_duration, Easing.OutQuint); this.FadeOut(fade_duration, Easing.OutQuint); + + if (wasOpened) + samplePopOut?.Play(); } protected override bool OnKeyDown(KeyDownEvent e) diff --git a/osu.Game/Online/API/Requests/DownloadReplayRequest.cs b/osu.Game/Online/API/Requests/DownloadReplayRequest.cs index 77174f0bb5..3ea57cf637 100644 --- a/osu.Game/Online/API/Requests/DownloadReplayRequest.cs +++ b/osu.Game/Online/API/Requests/DownloadReplayRequest.cs @@ -14,6 +14,6 @@ namespace osu.Game.Online.API.Requests protected override string FileExtension => ".osr"; - protected override string Target => $@"scores/{Model.Ruleset.ShortName}/{Model.OnlineID}/download"; + protected override string Target => $@"scores/{Model.OnlineID}/download"; } } diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 87df08ceec..a47d10c565 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -55,6 +55,9 @@ namespace osu.Game.Overlays private const float side_bar_width = 190; private const float chat_bar_height = 60; + protected override string PopInSampleName => @"UI/overlay-big-pop-in"; + protected override string PopOutSampleName => @"UI/overlay-big-pop-out"; + [Resolved] private OsuConfigManager config { get; set; } = null!; diff --git a/osu.Game/Overlays/LoginOverlay.cs b/osu.Game/Overlays/LoginOverlay.cs index a575253e71..8a4bda89d9 100644 --- a/osu.Game/Overlays/LoginOverlay.cs +++ b/osu.Game/Overlays/LoginOverlay.cs @@ -20,6 +20,8 @@ namespace osu.Game.Overlays private const float transition_time = 400; + protected override double PopInOutSampleBalance => OsuGameBase.SFX_STEREO_STRENGTH; + [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); diff --git a/osu.Game/Overlays/Mods/AddPresetButton.cs b/osu.Game/Overlays/Mods/AddPresetButton.cs index 731079d1d9..276afd9bec 100644 --- a/osu.Game/Overlays/Mods/AddPresetButton.cs +++ b/osu.Game/Overlays/Mods/AddPresetButton.cs @@ -18,6 +18,8 @@ namespace osu.Game.Overlays.Mods { public partial class AddPresetButton : ShearedToggleButton, IHasPopover { + protected override bool PlayToggleSamples => false; + [Resolved] private OsuColour colours { get; set; } = null!; diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index b93d5f1e12..6e0ea23dd1 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -31,6 +31,8 @@ namespace osu.Game.Overlays public LocalisableString Title => NotificationsStrings.HeaderTitle; public LocalisableString Description => NotificationsStrings.HeaderDescription; + protected override double PopInOutSampleBalance => OsuGameBase.SFX_STEREO_STRENGTH; + public const float WIDTH = 320; public const float TRANSITION_LENGTH = 600; diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index d7f39a9d8f..58c56a5514 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -56,6 +56,7 @@ namespace osu.Game.Overlays private SeekLimitedSearchTextBox searchTextBox; protected override string PopInSampleName => "UI/settings-pop-in"; + protected override double PopInOutSampleBalance => -OsuGameBase.SFX_STEREO_STRENGTH; private readonly bool showSidebar; diff --git a/osu.Game/Overlays/WaveOverlayContainer.cs b/osu.Game/Overlays/WaveOverlayContainer.cs index 153f7f5412..0295ff467a 100644 --- a/osu.Game/Overlays/WaveOverlayContainer.cs +++ b/osu.Game/Overlays/WaveOverlayContainer.cs @@ -18,7 +18,9 @@ namespace osu.Game.Overlays protected override bool StartHidden => true; - protected override string PopInSampleName => "UI/wave-pop-in"; + // `WaveContainer` plays PopIn/PopOut samples, so we disable the overlay-level one as to not double-up sample playback. + protected override string PopInSampleName => string.Empty; + protected override string PopOutSampleName => string.Empty; public const float HORIZONTAL_PADDING = 50; diff --git a/osu.Game/Rulesets/Mods/ModExtensions.cs b/osu.Game/Rulesets/Mods/ModExtensions.cs index b22030414b..aa106f1203 100644 --- a/osu.Game/Rulesets/Mods/ModExtensions.cs +++ b/osu.Game/Rulesets/Mods/ModExtensions.cs @@ -21,8 +21,9 @@ namespace osu.Game.Rulesets.Mods { User = new APIUser { - Id = APIUser.SYSTEM_USER_ID, + Id = replayData.User.OnlineID, Username = replayData.User.Username, + IsBot = replayData.User.IsBot, } } }; diff --git a/osu.Game/Rulesets/Mods/ModScoreV2.cs b/osu.Game/Rulesets/Mods/ModScoreV2.cs index 6d56b2d86f..df83d96769 100644 --- a/osu.Game/Rulesets/Mods/ModScoreV2.cs +++ b/osu.Game/Rulesets/Mods/ModScoreV2.cs @@ -16,5 +16,6 @@ namespace osu.Game.Rulesets.Mods public override ModType Type => ModType.System; public override LocalisableString Description => "Score set on earlier osu! versions with the V2 scoring algorithm active."; public override double ScoreMultiplier => 1; + public override bool UserPlayable => false; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs index 59b0bd1785..e36f1e9cad 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BeatDivisorControl.cs @@ -262,6 +262,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private readonly OsuSpriteText divisorText; public DivisorDisplay() + : base(HoverSampleSet.Default) { Anchor = Anchor.Centre; Origin = Anchor.Centre; diff --git a/osu.Game/Screens/Edit/Setup/LabelledFileChooser.cs b/osu.Game/Screens/Edit/Setup/LabelledFileChooser.cs index d14357e875..61f33c4bdc 100644 --- a/osu.Game/Screens/Edit/Setup/LabelledFileChooser.cs +++ b/osu.Game/Screens/Edit/Setup/LabelledFileChooser.cs @@ -114,6 +114,9 @@ namespace osu.Game.Screens.Edit.Setup private partial class FileChooserPopover : OsuPopover { + protected override string PopInSampleName => "UI/overlay-big-pop-in"; + protected override string PopOutSampleName => "UI/overlay-big-pop-out"; + public FileChooserPopover(string[] handledExtensions, Bindable currentFile, string? chooserPath) { Child = new Container diff --git a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs index 70e4b2a589..5cf2f91ff4 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/DrawableLoungeRoom.cs @@ -170,7 +170,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge if (Room.HasPassword.Value) { - sampleJoin?.Play(); this.ShowPopover(); return true; } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 1d0499c909..03231f9329 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -156,7 +156,7 @@ namespace osu.Game.Screens.Ranking if (Score != null) { // only show flair / animation when arriving after watching a play that isn't autoplay. - bool shouldFlair = player != null && Score.Mods.All(m => m.UserPlayable); + bool shouldFlair = player != null && !Score.User.IsBot; ScorePanelList.AddScore(Score, shouldFlair); } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 9af9a0ce72..9ea94b2a52 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -78,6 +78,8 @@ namespace osu.Game.Screens.Select private CarouselBeatmapSet? selectedBeatmapSet; + private List originalBeatmapSetsDetached = new List(); + /// /// Raised when the is changed. /// @@ -127,15 +129,37 @@ namespace osu.Game.Screens.Select private void loadBeatmapSets(IEnumerable beatmapSets) { + originalBeatmapSetsDetached = beatmapSets.Detach(); + + if (selectedBeatmapSet != null && !originalBeatmapSetsDetached.Contains(selectedBeatmapSet.BeatmapSet)) + selectedBeatmapSet = null; + + var selectedBeatmapBefore = selectedBeatmap?.BeatmapInfo; + CarouselRoot newRoot = new CarouselRoot(this); - newRoot.AddItems(beatmapSets.Select(s => createCarouselSet(s.Detach())).OfType()); + if (beatmapsSplitOut) + { + var carouselBeatmapSets = originalBeatmapSetsDetached.SelectMany(s => s.Beatmaps).Select(b => + { + return createCarouselSet(new BeatmapSetInfo(new[] { b }) + { + ID = b.BeatmapSet!.ID, + OnlineID = b.BeatmapSet!.OnlineID + }); + }).OfType(); + + newRoot.AddItems(carouselBeatmapSets); + } + else + { + var carouselBeatmapSets = originalBeatmapSetsDetached.Select(createCarouselSet).OfType(); + + newRoot.AddItems(carouselBeatmapSets); + } root = newRoot; - if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet)) - selectedBeatmapSet = null; - Scroll.Clear(false); itemsCache.Invalidate(); ScrollToSelected(); @@ -144,6 +168,15 @@ namespace osu.Game.Screens.Select if (loadedTestBeatmaps) signalBeatmapsLoaded(); + + // Restore selection + if (selectedBeatmapBefore != null && newRoot.BeatmapSetsByID.TryGetValue(selectedBeatmapBefore.BeatmapSet!.ID, out var newSelectionCandidates)) + { + CarouselBeatmap? found = newSelectionCandidates.SelectMany(s => s.Beatmaps).SingleOrDefault(b => b.BeatmapInfo.ID == selectedBeatmapBefore.ID); + + if (found != null) + found.State.Value = CarouselItemState.Selected; + } } private readonly List visibleItems = new List(); @@ -330,8 +363,8 @@ namespace osu.Game.Screens.Select // Only require to action here if the beatmap is missing. // This avoids processing these events unnecessarily when new beatmaps are imported, for example. - if (root.BeatmapSetsByID.TryGetValue(beatmapSet.ID, out var existingSet) - && existingSet.BeatmapSet.Beatmaps.All(b => b.ID != beatmapInfo.ID)) + if (root.BeatmapSetsByID.TryGetValue(beatmapSet.ID, out var existingSets) + && existingSets.SelectMany(s => s.Beatmaps).All(b => b.BeatmapInfo.ID != beatmapInfo.ID)) { UpdateBeatmapSet(beatmapSet.Detach()); } @@ -345,15 +378,20 @@ namespace osu.Game.Screens.Select private void removeBeatmapSet(Guid beatmapSetID) => Schedule(() => { - if (!root.BeatmapSetsByID.TryGetValue(beatmapSetID, out var existingSet)) + if (!root.BeatmapSetsByID.TryGetValue(beatmapSetID, out var existingSets)) return; - foreach (var beatmap in existingSet.Beatmaps) - randomSelectedBeatmaps.Remove(beatmap); + originalBeatmapSetsDetached.RemoveAll(set => set.ID == beatmapSetID); - previouslyVisitedRandomSets.Remove(existingSet); + foreach (var set in existingSets) + { + foreach (var beatmap in set.Beatmaps) + randomSelectedBeatmaps.Remove(beatmap); + previouslyVisitedRandomSets.Remove(set); + + root.RemoveItem(set); + } - root.RemoveItem(existingSet); itemsCache.Invalidate(); if (!Scroll.UserScrolling) @@ -366,26 +404,63 @@ namespace osu.Game.Screens.Select { Guid? previouslySelectedID = null; + originalBeatmapSetsDetached.RemoveAll(set => set.ID == beatmapSet.ID); + originalBeatmapSetsDetached.Add(beatmapSet.Detach()); + // If the selected beatmap is about to be removed, store its ID so it can be re-selected if required if (selectedBeatmapSet?.BeatmapSet.ID == beatmapSet.ID) previouslySelectedID = selectedBeatmap?.BeatmapInfo.ID; - var newSet = createCarouselSet(beatmapSet); - var removedSet = root.RemoveChild(beatmapSet.ID); + var removedSets = root.RemoveItemsByID(beatmapSet.ID); - // If we don't remove this here, it may remain in a hidden state until scrolled off screen. - // Doesn't really affect anything during actual user interaction, but makes testing annoying. - var removedDrawable = Scroll.FirstOrDefault(c => c.Item == removedSet); - if (removedDrawable != null) - expirePanelImmediately(removedDrawable); - - if (newSet != null) + foreach (var removedSet in removedSets) { - root.AddItem(newSet); + // If we don't remove this here, it may remain in a hidden state until scrolled off screen. + // Doesn't really affect anything during actual user interaction, but makes testing annoying. + var removedDrawable = Scroll.FirstOrDefault(c => c.Item == removedSet); + if (removedDrawable != null) + expirePanelImmediately(removedDrawable); + } + + if (beatmapsSplitOut) + { + var newSets = new List(); + + foreach (var beatmap in beatmapSet.Beatmaps) + { + var newSet = createCarouselSet(new BeatmapSetInfo(new[] { beatmap }) + { + ID = beatmapSet.ID, + OnlineID = beatmapSet.OnlineID + }); + + if (newSet != null) + { + newSets.Add(newSet); + root.AddItem(newSet); + } + } // check if we can/need to maintain our current selection. if (previouslySelectedID != null) - select((CarouselItem?)newSet.Beatmaps.FirstOrDefault(b => b.BeatmapInfo.ID == previouslySelectedID) ?? newSet); + { + var toSelect = newSets.FirstOrDefault(s => s.Beatmaps.Any(b => b.BeatmapInfo.ID == previouslySelectedID)) + ?? newSets.FirstOrDefault(); + select(toSelect); + } + } + else + { + var newSet = createCarouselSet(beatmapSet); + + if (newSet != null) + { + root.AddItem(newSet); + + // check if we can/need to maintain our current selection. + if (previouslySelectedID != null) + select((CarouselItem?)newSet.Beatmaps.FirstOrDefault(b => b.BeatmapInfo.ID == previouslySelectedID) ?? newSet); + } } itemsCache.Invalidate(); @@ -632,6 +707,8 @@ namespace osu.Game.Screens.Select applyActiveCriteria(debounce); } + private bool beatmapsSplitOut; + private void applyActiveCriteria(bool debounce, bool alwaysResetScrollPosition = true) { PendingFilter?.Cancel(); @@ -652,6 +729,13 @@ namespace osu.Game.Screens.Select { PendingFilter = null; + if (activeCriteria.SplitOutDifficulties != beatmapsSplitOut) + { + beatmapsSplitOut = activeCriteria.SplitOutDifficulties; + loadBeatmapSets(originalBeatmapSetsDetached); + return; + } + root.Filter(activeCriteria); itemsCache.Invalidate(); @@ -1055,7 +1139,7 @@ namespace osu.Game.Screens.Select // May only be null during construction (State.Value set causes PerformSelection to be triggered). private readonly BeatmapCarousel? carousel; - public readonly Dictionary BeatmapSetsByID = new Dictionary(); + public readonly Dictionary> BeatmapSetsByID = new Dictionary>(); public CarouselRoot(BeatmapCarousel carousel) { @@ -1069,20 +1153,25 @@ namespace osu.Game.Screens.Select public override void AddItem(CarouselItem i) { CarouselBeatmapSet set = (CarouselBeatmapSet)i; - BeatmapSetsByID.Add(set.BeatmapSet.ID, set); + if (BeatmapSetsByID.TryGetValue(set.BeatmapSet.ID, out var sets)) + sets.Add(set); + else + BeatmapSetsByID.Add(set.BeatmapSet.ID, new List { set }); base.AddItem(i); } - public CarouselBeatmapSet? RemoveChild(Guid beatmapSetID) + public IEnumerable RemoveItemsByID(Guid beatmapSetID) { - if (BeatmapSetsByID.TryGetValue(beatmapSetID, out var carouselBeatmapSet)) + if (BeatmapSetsByID.TryGetValue(beatmapSetID, out var carouselBeatmapSets)) { - RemoveItem(carouselBeatmapSet); - return carouselBeatmapSet; + foreach (var set in carouselBeatmapSets) + RemoveItem(set); + + return carouselBeatmapSets; } - return null; + return Enumerable.Empty(); } public override void RemoveItem(CarouselItem i) diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index ab4f85fc92..a2ae114126 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -19,6 +19,11 @@ namespace osu.Game.Screens.Select public GroupMode Group; public SortMode Sort; + /// + /// Whether the display of beatmap sets should be split apart per-difficulty for the current criteria. + /// + public bool SplitOutDifficulties => Sort == SortMode.Difficulty; + public BeatmapSetInfo? SelectedBeatmapSet; public OptionalRange StarDifficulty; diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs index 5753c268d9..7b631ebfea 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs @@ -32,6 +32,9 @@ namespace osu.Game.Screens.Select.Options public override bool BlockScreenWideMouse => false; + protected override string PopInSampleName => "SongSelect/options-pop-in"; + protected override string PopOutSampleName => "SongSelect/options-pop-out"; + public BeatmapOptionsOverlay() { AutoSizeAxes = Axes.Y; diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index ef39c12768..c2778ca5b1 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -170,7 +170,7 @@ ERROR WARNING WARNING - HINT + DO_NOT_SHOW WARNING WARNING WARNING