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