diff --git a/.github/ISSUE_TEMPLATE/bug-issue.yml b/.github/ISSUE_TEMPLATE/bug-issue.yml index ff6d869e7200..00a873f9c8b6 100644 --- a/.github/ISSUE_TEMPLATE/bug-issue.yml +++ b/.github/ISSUE_TEMPLATE/bug-issue.yml @@ -46,22 +46,16 @@ body: value: | ## Logs - Attaching log files is required for every reported bug. See instructions below on how to find them. - - **Logs are reset when you reopen the game.** If the game crashed or has been closed since you found the bug, retrieve the logs using the file explorer instead. + Attaching log files is required for **every** issue, regardless of whether you deem them required or not. See instructions below on how to find them. ### Desktop platforms If the game has not yet been closed since you found the bug: - 1. Head on to game settings and click on "Open osu! folder" - 2. Then open the `logs` folder located there - - The default places to find the logs on desktop platforms are as follows: - - `%AppData%/osu/logs` *on Windows* - - `~/.local/share/osu/logs` *on Linux* - - `~/Library/Application Support/osu/logs` *on macOS* + 1. Head on to game settings and click on "Export logs" + 2. Click the notification to locate the file + 3. Drag the generated `.zip` files into the github issue window - If you have selected a custom location for the game files, you can find the `logs` folder there. + ![export logs button](https://github.com/ppy/osu/assets/191335/cbfa5550-b7ed-4c5c-8dd0-8b87cc90ad9b) ### Mobile platforms @@ -69,10 +63,6 @@ body: - *On Android*, navigate to `Android/data/sh.ppy.osulazer/files/logs` using a file browser app. - *On iOS*, connect your device to a PC and copy the `logs` directory from the app's document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer) - --- - - After locating the `logs` folder, select all log files inside and drag them into the "Logs" box below. - - type: textarea attributes: label: Logs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderEarlyHitJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderEarlyHitJudgement.cs index 9caee86a3250..4ea21e51f66a 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderEarlyHitJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderEarlyHitJudgement.cs @@ -6,6 +6,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Screens; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Replays; using osu.Game.Rulesets.Judgements; @@ -160,6 +161,10 @@ private void performTest(List frames, Action? adjustSliderF Position = new Vector2(256 - slider_path_length / 2, 192), TickDistanceMultiplier = 3, ClassicSliderBehaviour = classic, + Samples = new[] + { + new HitSampleInfo(HitSampleInfo.HIT_NORMAL) + }, Path = new SliderPath(PathType.LINEAR, new[] { Vector2.Zero, diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 1f9a02804598..b306fd38c19e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -128,8 +128,6 @@ private void load() foreach (var drawableHitObject in NestedHitObjects) drawableHitObject.AccentColour.Value = colour.NewValue; }, true); - - Tracking.BindValueChanged(updateSlidingSample); } protected override void OnApply() @@ -166,14 +164,6 @@ public override void StopAllSamples() slidingSample?.Stop(); } - private void updateSlidingSample(ValueChangedEvent tracking) - { - if (tracking.NewValue) - slidingSample?.Play(); - else - slidingSample?.Stop(); - } - protected override void AddNestedHitObject(DrawableHitObject hitObject) { base.AddNestedHitObject(hitObject); @@ -238,9 +228,18 @@ protected override void Update() Tracking.Value = SliderInputManager.Tracking; - if (Tracking.Value && slidingSample != null) - // keep the sliding sample playing at the current tracking position - slidingSample.Balance.Value = CalculateSamplePlaybackBalance(CalculateDrawableRelativePosition(Ball)); + if (slidingSample != null) + { + if (Tracking.Value && Time.Current >= HitObject.StartTime) + { + // keep the sliding sample playing at the current tracking position + if (!slidingSample.IsPlaying) + slidingSample.Play(); + slidingSample.Balance.Value = CalculateSamplePlaybackBalance(CalculateDrawableRelativePosition(Ball)); + } + else if (slidingSample.IsPlaying) + slidingSample.Stop(); + } double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1); diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 6b53277964e3..518035fb822a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -464,7 +464,7 @@ public void TestSelectionRetainedOnBeatmapUpdate() manager.Import(testBeatmapSetInfo); }, 10); - AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID == originalOnlineSetID); + AddUntilStep("has selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID, () => Is.EqualTo(originalOnlineSetID)); Task?> updateTask = null!; @@ -476,7 +476,7 @@ public void TestSelectionRetainedOnBeatmapUpdate() }); AddUntilStep("wait for update completion", () => updateTask.IsCompleted); - AddUntilStep("retained selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID == originalOnlineSetID); + AddUntilStep("retained selection", () => songSelect!.Carousel.SelectedBeatmapInfo?.BeatmapSet?.OnlineID, () => Is.EqualTo(originalOnlineSetID)); } [Test] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 80be4412b39e..4b101a52f42a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -572,7 +572,7 @@ void navigateAndClick() where T : Drawable [Test] public void TestTextSearchActiveByDefault() { - configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, true); + AddStep("text search starts active", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, true)); createScreen(); AddUntilStep("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus); @@ -587,7 +587,7 @@ public void TestTextSearchActiveByDefault() [Test] public void TestTextSearchNotActiveByDefault() { - configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, false); + AddStep("text search does not start active", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, false)); createScreen(); AddUntilStep("search text box not focused", () => !modSelectOverlay.SearchTextBox.HasFocus); @@ -599,6 +599,31 @@ public void TestTextSearchNotActiveByDefault() AddAssert("search text box unfocused", () => !modSelectOverlay.SearchTextBox.HasFocus); } + [Test] + public void TestTextSearchDoesNotBlockCustomisationPanelKeyboardInteractions() + { + AddStep("text search starts active", () => configManager.SetValue(OsuSetting.ModSelectTextSearchStartsActive, true)); + createScreen(); + + AddUntilStep("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus); + + AddStep("select DT", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime() }); + AddAssert("DT selected", () => modSelectOverlay.ChildrenOfType().Count(panel => panel.Active.Value), () => Is.EqualTo(1)); + + AddStep("open customisation area", () => modSelectOverlay.CustomisationButton!.TriggerClick()); + assertCustomisationToggleState(false, true); + AddStep("hover over mod settings slider", () => + { + var slider = modSelectOverlay.ChildrenOfType().Single().ChildrenOfType>().First(); + InputManager.MoveMouseTo(slider); + }); + AddStep("press right arrow", () => InputManager.PressKey(Key.Right)); + AddAssert("DT speed changed", () => !SelectedMods.Value.OfType().Single().SpeedChange.IsDefault); + + AddStep("close customisation area", () => InputManager.PressKey(Key.Escape)); + AddUntilStep("search text box reacquired focus", () => modSelectOverlay.SearchTextBox.HasFocus); + } + [Test] public void TestDeselectAllViaKey() { diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 8d9e029c9d1b..e7ff99ef01ea 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1190,7 +1190,7 @@ private void forwardGeneralLogsToNotifications() } else if (recentLogCount == short_term_display_limit) { - string logFile = $@"{entry.Target.Value.ToString().ToLowerInvariant()}.log"; + string logFile = Logger.GetLogger(entry.Target.Value).Filename; Schedule(() => Notifications.Post(new SimpleNotification { @@ -1198,7 +1198,7 @@ private void forwardGeneralLogsToNotifications() Text = NotificationsStrings.SubsequentMessagesLogged, Activated = () => { - Storage.GetStorageForDirectory(@"logs").PresentFileExternally(logFile); + Logger.Storage.PresentFileExternally(logFile); return true; } })); diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index ce798ae75208..baa7e594c18e 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -132,6 +132,8 @@ protected virtual IEnumerable CreateFooterButtons() protected ShearedToggleButton? CustomisationButton { get; private set; } protected SelectAllModsButton? SelectAllModsButton { get; set; } + private bool textBoxShouldFocus; + private Sample? columnAppearSample; private WorkingBeatmap? beatmap; @@ -508,6 +510,11 @@ private void updateCustomisationVisualState() modSettingsArea.ResizeHeightTo(modAreaHeight, transition_duration, Easing.InOutCubic); TopLevelContent.MoveToY(-modAreaHeight, transition_duration, Easing.InOutCubic); + + if (customisationVisible.Value) + SearchTextBox.KillFocus(); + else + setTextBoxFocus(textBoxShouldFocus); } /// @@ -621,8 +628,7 @@ protected override void PopIn() nonFilteredColumnCount += 1; } - if (textSearchStartsActive.Value) - SearchTextBox.TakeFocus(); + setTextBoxFocus(textSearchStartsActive.Value); } protected override void PopOut() @@ -761,14 +767,20 @@ protected override bool OnKeyDown(KeyDownEvent e) return false; // TODO: should probably eventually support typical platform search shortcuts (`Ctrl-F`, `/`) - if (SearchTextBox.HasFocus) - SearchTextBox.KillFocus(); - else - SearchTextBox.TakeFocus(); - + setTextBoxFocus(!textBoxShouldFocus); return true; } + private void setTextBoxFocus(bool keepFocus) + { + textBoxShouldFocus = keepFocus; + + if (textBoxShouldFocus) + SearchTextBox.TakeFocus(); + else + SearchTextBox.KillFocus(); + } + #endregion #region Sample playback control diff --git a/osu.Game/Overlays/Mods/ModSettingsArea.cs b/osu.Game/Overlays/Mods/ModSettingsArea.cs index 6158c2c70f16..54bfcc719974 100644 --- a/osu.Game/Overlays/Mods/ModSettingsArea.cs +++ b/osu.Game/Overlays/Mods/ModSettingsArea.cs @@ -32,6 +32,8 @@ public partial class ModSettingsArea : CompositeDrawable [Resolved] private OverlayColourProvider colourProvider { get; set; } = null!; + public override bool AcceptsFocus => true; + public ModSettingsArea() { RelativeSizeAxes = Axes.X; diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index eb47a7201a99..fe7eee701f5f 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -64,7 +64,7 @@ public partial class BeatmapCarousel : CompositeDrawable, IKeyBindingHandler /// The total count of non-filtered beatmaps displayed. /// - public int CountDisplayed => beatmapSets.Where(s => !s.Filtered.Value).Sum(s => s.Beatmaps.Count(b => !b.Filtered.Value)); + public int CountDisplayed => beatmapSets.Where(s => !s.Filtered.Value).Sum(s => s.TotalItemsNotFiltered); /// /// The currently selected beatmap set. @@ -168,7 +168,10 @@ private void loadBeatmapSets(IEnumerable beatmapSets) applyActiveCriteria(false); if (loadedTestBeatmaps) - signalBeatmapsLoaded(); + { + invalidateAfterChange(); + BeatmapSetsLoaded = true; + } // Restore selection if (selectedBeatmapBefore != null && newRoot.BeatmapSetsByID.TryGetValue(selectedBeatmapBefore.BeatmapSet!.ID, out var newSelectionCandidates)) @@ -266,8 +269,30 @@ private void deletedBeatmapSetsChanged(IRealmCollection sender, if (changes == null) return; - foreach (int i in changes.InsertedIndices) - removeBeatmapSet(sender[i].ID); + var removeableSets = changes.InsertedIndices.Select(i => sender[i].ID).ToHashSet(); + + // This schedule is required to retain selection of beatmaps over an ImportAsUpdate operation. + // This is covered by TestPlaySongSelect.TestSelectionRetainedOnBeatmapUpdate. + // + // In short, we have specialised logic in `beatmapSetsChanged` (directly below) to infer that an + // update operation has occurred. For this to work, we need to confirm the `DeletePending` flag + // of the current selection. + // + // If we don't schedule the following code, it is possible for the `deleteBeatmapSetsChanged` handler + // to be invoked before the `beatmapSetsChanged` handler (realm call order seems non-deterministic) + // which will lead to the currently selected beatmap changing via `CarouselGroupEagerSelect`. + // + // We need a better path forward here. A few ideas: + // - Avoid the necessity of having realm subscriptions on deleted/hidden items, maybe by storing all guids in realm + // to a local list so we can better look them up on receiving `DeletedIndices`. + // - Add a new property on `BeatmapSetInfo` to link to the pre-update set, and use that to handle the update case. + Schedule(() => + { + foreach (var set in removeableSets) + removeBeatmapSet(set); + + invalidateAfterChange(); + }); } private void beatmapSetsChanged(IRealmCollection sender, ChangeSet? changes) @@ -289,7 +314,7 @@ private void beatmapSetsChanged(IRealmCollection sender, ChangeS foreach (var id in realmSets) { if (!root.BeatmapSetsByID.ContainsKey(id)) - UpdateBeatmapSet(realm.Realm.Find(id)!.Detach()); + updateBeatmapSet(realm.Realm.Find(id)!.Detach()); } foreach (var id in root.BeatmapSetsByID.Keys) @@ -298,15 +323,16 @@ private void beatmapSetsChanged(IRealmCollection sender, ChangeS removeBeatmapSet(id); } - signalBeatmapsLoaded(); + invalidateAfterChange(); + BeatmapSetsLoaded = true; return; } foreach (int i in changes.NewModifiedIndices) - UpdateBeatmapSet(sender[i].Detach()); + updateBeatmapSet(sender[i].Detach()); foreach (int i in changes.InsertedIndices) - UpdateBeatmapSet(sender[i].Detach()); + updateBeatmapSet(sender[i].Detach()); if (changes.DeletedIndices.Length > 0 && SelectedBeatmapInfo != null) { @@ -316,7 +342,7 @@ private void beatmapSetsChanged(IRealmCollection sender, ChangeS // To handle the beatmap update flow, attempt to track selection changes across delete-insert transactions. // When an update occurs, the previous beatmap set is either soft or hard deleted. // Check if the current selection was potentially deleted by re-querying its validity. - bool selectedSetMarkedDeleted = realm.Run(r => r.Find(SelectedBeatmapSet.ID))?.DeletePending != false; + bool selectedSetMarkedDeleted = sender.Realm.Find(SelectedBeatmapSet.ID)?.DeletePending != false; int[] modifiedAndInserted = changes.NewModifiedIndices.Concat(changes.InsertedIndices).ToArray(); @@ -347,6 +373,8 @@ private void beatmapSetsChanged(IRealmCollection sender, ChangeS SelectBeatmap(sender[modifiedAndInserted.First()].Beatmaps.First()); } } + + invalidateAfterChange(); } private void beatmapsChanged(IRealmCollection sender, ChangeSet? changes) @@ -355,6 +383,8 @@ private void beatmapsChanged(IRealmCollection sender, ChangeSet? ch if (changes == null) return; + bool changed = false; + foreach (int i in changes.InsertedIndices) { var beatmapInfo = sender[i]; @@ -367,17 +397,24 @@ private void beatmapsChanged(IRealmCollection sender, ChangeSet? ch if (root.BeatmapSetsByID.TryGetValue(beatmapSet.ID, out var existingSets) && existingSets.SelectMany(s => s.Beatmaps).All(b => b.BeatmapInfo.ID != beatmapInfo.ID)) { - UpdateBeatmapSet(beatmapSet.Detach()); + updateBeatmapSet(beatmapSet.Detach()); + changed = true; } } + + if (changed) + invalidateAfterChange(); } private IQueryable getBeatmapSets(Realm realm) => realm.All().Where(s => !s.DeletePending && !s.Protected); - public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => + public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => + { removeBeatmapSet(beatmapSet.ID); + invalidateAfterChange(); + }); - private void removeBeatmapSet(Guid beatmapSetID) => Schedule(() => + private void removeBeatmapSet(Guid beatmapSetID) { if (!root.BeatmapSetsByID.TryGetValue(beatmapSetID, out var existingSets)) return; @@ -392,16 +429,15 @@ private void removeBeatmapSet(Guid beatmapSetID) => Schedule(() => root.RemoveItem(set); } + } - itemsCache.Invalidate(); - - if (!Scroll.UserScrolling) - ScrollToSelected(true); - - BeatmapSetsChanged?.Invoke(); + public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => + { + updateBeatmapSet(beatmapSet); + invalidateAfterChange(); }); - public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => + private void updateBeatmapSet(BeatmapSetInfo beatmapSet) { Guid? previouslySelectedID = null; @@ -464,14 +500,7 @@ public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() => select((CarouselItem?)newSet.Beatmaps.FirstOrDefault(b => b.BeatmapInfo.ID == previouslySelectedID) ?? newSet); } } - - itemsCache.Invalidate(); - - if (!Scroll.UserScrolling) - ScrollToSelected(true); - - BeatmapSetsChanged?.Invoke(); - }); + } /// /// Selects a given beatmap on the carousel. @@ -748,15 +777,14 @@ void perform() } } - private void signalBeatmapsLoaded() + private void invalidateAfterChange() { - if (!BeatmapSetsLoaded) - { - BeatmapSetsChanged?.Invoke(); - BeatmapSetsLoaded = true; - } - itemsCache.Invalidate(); + + if (!Scroll.UserScrolling) + ScrollToSelected(true); + + BeatmapSetsChanged?.Invoke(); } private float? scrollTarget; diff --git a/osu.Game/Screens/Select/Carousel/CarouselGroup.cs b/osu.Game/Screens/Select/Carousel/CarouselGroup.cs index c353ee98ae3f..b2ca117cec3d 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselGroup.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselGroup.cs @@ -14,6 +14,8 @@ public class CarouselGroup : CarouselItem public IReadOnlyList Items => items; + public int TotalItemsNotFiltered { get; private set; } + private readonly List items = new List(); /// @@ -31,6 +33,9 @@ public virtual void RemoveItem(CarouselItem i) { items.Remove(i); + if (!i.Filtered.Value) + TotalItemsNotFiltered--; + // it's important we do the deselection after removing, so any further actions based on // State.ValueChanged make decisions post-removal. i.State.Value = CarouselItemState.Collapsed; @@ -55,6 +60,9 @@ public virtual void AddItem(CarouselItem i) // criteria may be null for initial population. the filtering will be applied post-add. items.Add(i); } + + if (!i.Filtered.Value) + TotalItemsNotFiltered++; } public CarouselGroup(List? items = null) @@ -84,10 +92,17 @@ public override void Filter(FilterCriteria criteria) { base.Filter(criteria); - items.ForEach(c => c.Filter(criteria)); + TotalItemsNotFiltered = 0; + + foreach (var c in items) + { + c.Filter(criteria); + if (!c.Filtered.Value) + TotalItemsNotFiltered++; + } // Sorting is expensive, so only perform if it's actually changed. - if (lastCriteria?.Sort != criteria.Sort) + if (lastCriteria?.RequiresSorting(criteria) != false) { criteriaComparer = Comparer.Create((x, y) => { diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 811f623ee54c..a7c8e7d09372 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -219,6 +219,44 @@ public string SearchTerm public bool Equals(OptionalTextFilter other) => SearchTerm == other.SearchTerm; } + /// + /// Given a new filter criteria, decide whether a full sort needs to be performed. + /// + /// + /// + public bool RequiresSorting(FilterCriteria newCriteria) + { + if (Sort != newCriteria.Sort) + return true; + + switch (Sort) + { + // Some sorts are stable across all other changes. + // Running these sorts will sort all items, including currently hidden items. + case SortMode.Artist: + case SortMode.Author: + case SortMode.DateSubmitted: + case SortMode.DateAdded: + case SortMode.DateRanked: + case SortMode.Source: + case SortMode.Title: + return false; + + // Some sorts use aggregate max comparisons, which will change based on filtered items. + // These sorts generally ignore items hidden by filtered state, so we must force a sort under all circumstances here. + // + // This makes things very slow when typing a text search, and we probably want to consider a way to optimise things going forward. + case SortMode.LastPlayed: + case SortMode.BPM: + case SortMode.Length: + case SortMode.Difficulty: + return true; + + default: + throw new ArgumentOutOfRangeException(nameof(Sort), Sort, "Unknown sort mode"); + } + } + public enum MatchMode { /// diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index dfea4e37942f..d23a660ff682 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -162,7 +162,7 @@ private void load(AudioManager audio, OsuColour colours, ManageCollectionsDialog BleedBottom = Footer.HEIGHT, SelectionChanged = updateSelectedBeatmap, BeatmapSetsChanged = carouselBeatmapsLoaded, - FilterApplied = updateVisibleBeatmapCount, + FilterApplied = () => Scheduler.AddOnce(updateVisibleBeatmapCount), GetRecommendedBeatmap = s => recommender?.GetRecommendedBeatmap(s), }, c => carouselContainer.Child = c); @@ -843,7 +843,7 @@ private void ensurePlayingSelected() private void carouselBeatmapsLoaded() { bindBindables(); - updateVisibleBeatmapCount(); + Scheduler.AddOnce(updateVisibleBeatmapCount); Carousel.AllowSelection = true; @@ -877,7 +877,8 @@ private void updateVisibleBeatmapCount() { // Intentionally not localised until we have proper support for this (see https://github.com/ppy/osu-framework/pull/4918 // but also in this case we want support for formatting a number within a string). - FilterControl.InformationalText = Carousel.CountDisplayed != 1 ? $"{Carousel.CountDisplayed:#,0} matches" : $"{Carousel.CountDisplayed:#,0} match"; + int carouselCountDisplayed = Carousel.CountDisplayed; + FilterControl.InformationalText = carouselCountDisplayed != 1 ? $"{carouselCountDisplayed:#,0} matches" : $"{carouselCountDisplayed:#,0} match"; } private bool boundLocalBindables;