diff --git a/osu.Game/Beatmaps/Drawables/BeatmapGroup.cs b/osu.Game/Beatmaps/Drawables/BeatmapGroup.cs index 2cccbc322a..ac0ab9966f 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapGroup.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapGroup.cs @@ -10,7 +10,7 @@ using osu.Game.Database; namespace osu.Game.Beatmaps.Drawables { - internal class BeatmapGroup : IStateful + public class BeatmapGroup : IStateful { public BeatmapPanel SelectedPanel; @@ -63,8 +63,6 @@ namespace osu.Game.Beatmaps.Drawables { BeatmapSet = beatmapSet; WorkingBeatmap beatmap = database.GetWorkingBeatmap(BeatmapSet.Beatmaps.FirstOrDefault()); - foreach (var b in BeatmapSet.Beatmaps) - b.StarDifficulty = (float)(database.GetWorkingBeatmap(b).Beatmap?.CalculateStarDifficulty() ?? -1f); Header = new BeatmapSetHeader(beatmap) { diff --git a/osu.Game/Beatmaps/Drawables/BeatmapPanel.cs b/osu.Game/Beatmaps/Drawables/BeatmapPanel.cs index aa5891c37e..67ebb2fcb9 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapPanel.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapPanel.cs @@ -18,7 +18,7 @@ using osu.Game.Graphics.Sprites; namespace osu.Game.Beatmaps.Drawables { - internal class BeatmapPanel : Panel + public class BeatmapPanel : Panel { public BeatmapInfo Beatmap; private Sprite background; @@ -59,6 +59,8 @@ namespace osu.Game.Beatmaps.Drawables protected override void ApplyState(PanelSelectedState last = PanelSelectedState.Hidden) { + if (!IsLoaded) return; + base.ApplyState(last); if (last == PanelSelectedState.Hidden && State != last) @@ -138,7 +140,7 @@ namespace osu.Game.Beatmaps.Drawables }, starCounter = new StarCounter { - Count = beatmap.StarDifficulty, + Count = (float)beatmap.StarDifficulty, Scale = new Vector2(0.8f), } } diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetHeader.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetHeader.cs index c1ac93f70c..3dc5fdedc9 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapSetHeader.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapSetHeader.cs @@ -17,7 +17,7 @@ using OpenTK.Graphics; namespace osu.Game.Beatmaps.Drawables { - internal class BeatmapSetHeader : Panel + public class BeatmapSetHeader : Panel { public Action GainedSelection; private SpriteText title, artist; @@ -32,10 +32,6 @@ namespace osu.Game.Beatmaps.Drawables Children = new Drawable[] { - new PanelBackground(beatmap) - { - RelativeSizeAxes = Axes.Both, - }, new FillFlowContainer { Direction = FillDirection.Vertical, @@ -74,13 +70,23 @@ namespace osu.Game.Beatmaps.Drawables } [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(OsuConfigManager config, OsuGameBase game) { this.config = config; preferUnicode = config.GetBindable(OsuConfig.ShowUnicode); preferUnicode.ValueChanged += preferUnicode_changed; preferUnicode_changed(preferUnicode, null); + + new PanelBackground(beatmap) + { + RelativeSizeAxes = Axes.Both, + Depth = 1, + }.LoadAsync(game, b => + { + Add(b); + b.FadeInFromZero(200); + }); } private void preferUnicode_changed(object sender, EventArgs e) @@ -98,16 +104,18 @@ namespace osu.Game.Beatmaps.Drawables private class PanelBackground : BufferedContainer { - private readonly WorkingBeatmap working; - public PanelBackground(WorkingBeatmap working) { - this.working = working; - CacheDrawnFrameBuffer = true; - Children = new[] + Children = new Drawable[] { + new BeatmapBackgroundSprite(working) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fill, + }, new FillFlowContainer { Depth = -1, @@ -151,21 +159,6 @@ namespace osu.Game.Beatmaps.Drawables }, }; } - - [BackgroundDependencyLoader] - private void load(OsuGameBase game) - { - new BeatmapBackgroundSprite(working) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - FillMode = FillMode.Fill, - }.LoadAsync(game, bg => - { - Add(bg); - ForceRedraw(); - }); - } } public void AddDifficultyIcons(IEnumerable panels) diff --git a/osu.Game/Beatmaps/Drawables/Panel.cs b/osu.Game/Beatmaps/Drawables/Panel.cs index a15d0c22f0..c51ae8680e 100644 --- a/osu.Game/Beatmaps/Drawables/Panel.cs +++ b/osu.Game/Beatmaps/Drawables/Panel.cs @@ -12,7 +12,7 @@ using osu.Framework.Extensions.Color4Extensions; namespace osu.Game.Beatmaps.Drawables { - internal class Panel : Container, IStateful + public class Panel : Container, IStateful { public const float MAX_HEIGHT = 80; @@ -51,6 +51,8 @@ namespace osu.Game.Beatmaps.Drawables protected virtual void ApplyState(PanelSelectedState last = PanelSelectedState.Hidden) { + if (!IsLoaded) return; + switch (state) { case PanelSelectedState.Hidden: @@ -115,7 +117,7 @@ namespace osu.Game.Beatmaps.Drawables } } - internal enum PanelSelectedState + public enum PanelSelectedState { Hidden, NotSelected, diff --git a/osu.Game/Database/BeatmapDatabase.cs b/osu.Game/Database/BeatmapDatabase.cs index b3d48c152f..3f865d65f1 100644 --- a/osu.Game/Database/BeatmapDatabase.cs +++ b/osu.Game/Database/BeatmapDatabase.cs @@ -123,16 +123,15 @@ namespace osu.Game.Database /// Multiple locations on disk public void Import(IEnumerable paths) { - Stack sets = new Stack(); - foreach (string p in paths) + { try { BeatmapSetInfo set = getBeatmapSet(p); //If we have an ID then we already exist in the database. if (set.ID == 0) - sets.Push(set); + Import(new[] { set }); // We may or may not want to delete the file depending on where it is stored. // e.g. reconstructing/repairing database with beatmaps from default storage. @@ -152,9 +151,7 @@ namespace osu.Game.Database e = e.InnerException ?? e; Logger.Error(e, @"Could not import beatmap set"); } - - // Batch commit with multiple sets to database - Import(sets); + } } /// @@ -236,6 +233,8 @@ namespace osu.Game.Database // TODO: Diff beatmap metadata with set metadata and leave it here if necessary beatmap.BeatmapInfo.Metadata = null; + beatmap.BeatmapInfo.StarDifficulty = beatmap.CalculateStarDifficulty(); + beatmapSet.Beatmaps.Add(beatmap.BeatmapInfo); } beatmapSet.StoryboardFile = archive.StoryboardFilename; diff --git a/osu.Game/Database/BeatmapInfo.cs b/osu.Game/Database/BeatmapInfo.cs index 5f9c0baee8..cda9cba70c 100644 --- a/osu.Game/Database/BeatmapInfo.cs +++ b/osu.Game/Database/BeatmapInfo.cs @@ -75,16 +75,7 @@ namespace osu.Game.Database // Metadata public string Version { get; set; } - private float starDifficulty = -1; - public float StarDifficulty - { - get - { - return starDifficulty < 0 ? (Difficulty?.OverallDifficulty ?? 5) : starDifficulty; - } - - set { starDifficulty = value; } - } + public double StarDifficulty { get; set; } public bool Equals(BeatmapInfo other) { diff --git a/osu.Game/Database/BeatmapSetInfo.cs b/osu.Game/Database/BeatmapSetInfo.cs index 12247c3997..0ef0ba4c63 100644 --- a/osu.Game/Database/BeatmapSetInfo.cs +++ b/osu.Game/Database/BeatmapSetInfo.cs @@ -24,7 +24,7 @@ namespace osu.Game.Database [OneToMany(CascadeOperations = CascadeOperation.All)] public List Beatmaps { get; set; } - public float MaxStarDifficulty => Beatmaps.Max(b => b.StarDifficulty); + public double MaxStarDifficulty => Beatmaps.Max(b => b.StarDifficulty); public bool DeletePending { get; set; } diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 33bffef219..71730bc0eb 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -27,6 +27,7 @@ namespace osu.Game.Screens.Menu internal override bool ShowOverlays => buttons.State != MenuState.Initial; private BackgroundScreen background; + private Screen songSelect; protected override BackgroundScreen CreateBackground() => background; @@ -46,7 +47,7 @@ namespace osu.Game.Screens.Menu OnChart = delegate { Push(new ChartListing()); }, OnDirect = delegate { Push(new OnlineListing()); }, OnEdit = delegate { Push(new Editor()); }, - OnSolo = delegate { Push(new PlaySongSelect()); }, + OnSolo = delegate { Push(consumeSongSelect()); }, OnMulti = delegate { Push(new Lobby()); }, OnTest = delegate { Push(new TestBrowser()); }, OnExit = delegate { Exit(); }, @@ -62,6 +63,24 @@ namespace osu.Game.Screens.Menu background.LoadAsync(game); buttons.OnSettings = game.ToggleOptions; + + preloadSongSelect(); + } + + private void preloadSongSelect() + { + if (songSelect == null) + { + songSelect = new PlaySongSelect(); + songSelect.LoadAsync(Game); + } + } + + private Screen consumeSongSelect() + { + var s = songSelect; + songSelect = null; + return s; } protected override void OnEntering(Screen last) @@ -86,6 +105,9 @@ namespace osu.Game.Screens.Menu { base.OnResuming(last); + //we may have consumed our preloaded instance, so let's make another. + preloadSongSelect(); + const float length = 300; buttons.State = MenuState.TopLevel; diff --git a/osu.Game/Screens/Select/CarouselContainer.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs similarity index 65% rename from osu.Game/Screens/Select/CarouselContainer.cs rename to osu.Game/Screens/Select/BeatmapCarousel.cs index 092e4461e0..b7405074ff 100644 --- a/osu.Game/Screens/Select/CarouselContainer.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -15,47 +15,234 @@ using OpenTK.Input; using System.Collections; using osu.Framework.MathUtils; using System.Diagnostics; -using osu.Game.Screens.Select.Filter; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Threading; namespace osu.Game.Screens.Select { - internal class CarouselContainer : ScrollContainer, IEnumerable + internal class BeatmapCarousel : ScrollContainer, IEnumerable { - private Container scrollableContent; - private List groups = new List(); - private List panels = new List(); + public BeatmapInfo SelectedBeatmap => selectedPanel?.Beatmap; - public BeatmapGroup SelectedGroup { get; private set; } - public BeatmapPanel SelectedPanel { get; private set; } + public Action BeatmapsChanged; + + public IEnumerable Beatmaps + { + get + { + return groups.Select(g => g.BeatmapSet); + } + + set + { + scrollableContent.Clear(false); + panels.Clear(); + groups.Clear(); + + List newGroups = null; + + Task.Run(() => + { + newGroups = value.Select(createGroup).ToList(); + criteria.Filter(newGroups); + }).ContinueWith(t => + { + Schedule(() => + { + foreach (var g in newGroups) + addGroup(g); + + computeYPositions(); + BeatmapsChanged?.Invoke(); + }); + }); + } + } private List yPositions = new List(); - public CarouselContainer() - { - DistanceDecayJump = 0.01; + /// + /// Required for now unfortunately. + /// + private BeatmapDatabase database; + private Container scrollableContent; + + private List groups = new List(); + + private List panels = new List(); + + private BeatmapGroup selectedGroup; + + private BeatmapPanel selectedPanel; + + public BeatmapCarousel() + { Add(scrollableContent = new Container { RelativeSizeAxes = Axes.X, }); } - public void AddGroup(BeatmapGroup group) + public void AddBeatmap(BeatmapSetInfo beatmapSet) { - groups.Add(group); + var group = createGroup(beatmapSet); - panels.Add(group.Header); - group.Header.UpdateClock(Clock); - foreach (BeatmapPanel panel in group.BeatmapPanels) + //for the time being, let's completely load the difficulty panels in the background. + //this likely won't scale so well, but allows us to completely async the loading flow. + Schedule(delegate { - panels.Add(panel); - panel.UpdateClock(Clock); - } - - computeYPositions(); + addGroup(group); + computeYPositions(); + if (selectedGroup == null) + selectGroup(group); + }); } - public void RemoveGroup(BeatmapGroup group) + public void SelectBeatmap(BeatmapInfo beatmap, bool animated = true) + { + if (beatmap == null) + { + SelectNext(); + return; + } + + foreach (BeatmapGroup group in groups) + { + var panel = group.BeatmapPanels.FirstOrDefault(p => p.Beatmap.Equals(beatmap)); + if (panel != null) + { + selectGroup(group, panel, animated); + return; + } + } + } + + public void RemoveBeatmap(BeatmapSetInfo info) => removeGroup(groups.Find(b => b.BeatmapSet.ID == info.ID)); + + public Action SelectionChanged; + + public Action StartRequested; + + public void SelectNext(int direction = 1, bool skipDifficulties = true) + { + if (groups.Count == 0) + { + selectedGroup = null; + selectedPanel = null; + return; + } + + if (!skipDifficulties && selectedGroup != null) + { + int i = selectedGroup.BeatmapPanels.IndexOf(selectedPanel) + direction; + + if (i >= 0 && i < selectedGroup.BeatmapPanels.Count) + { + //changing difficulty panel, not set. + selectGroup(selectedGroup, selectedGroup.BeatmapPanels[i]); + return; + } + } + + int startIndex = groups.IndexOf(selectedGroup); + int index = startIndex; + + do + { + index = (index + direction + groups.Count) % groups.Count; + if (groups[index].State != BeatmapGroupState.Hidden) + { + SelectBeatmap(groups[index].BeatmapPanels.First().Beatmap); + return; + } + } while (index != startIndex); + } + + public void SelectRandom() + { + List visibleGroups = groups.Where(selectGroup => selectGroup.State != BeatmapGroupState.Hidden).ToList(); + if (visibleGroups.Count < 1) + return; + BeatmapGroup group = visibleGroups[RNG.Next(visibleGroups.Count)]; + BeatmapPanel panel = group?.BeatmapPanels.First(); + + if (panel == null) + return; + + selectGroup(group, panel); + } + + private FilterCriteria criteria = new FilterCriteria(); + + private ScheduledDelegate filterTask; + + public void Filter(FilterCriteria newCriteria = null, bool debounce = true) + { + if (!IsLoaded) return; + + criteria = newCriteria ?? criteria ?? new FilterCriteria(); + + Action perform = delegate + { + filterTask = null; + + criteria.Filter(groups); + + var filtered = new List(groups); + + scrollableContent.Clear(false); + panels.Clear(); + groups.Clear(); + + foreach (var g in filtered) + addGroup(g); + + computeYPositions(); + + if (selectedGroup == null || selectedGroup.State == BeatmapGroupState.Hidden) + SelectNext(); + }; + + filterTask?.Cancel(); + if (debounce) + filterTask = Scheduler.AddDelayed(perform, 250); + else + perform(); + } + + public IEnumerator GetEnumerator() => groups.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private BeatmapGroup createGroup(BeatmapSetInfo beatmapSet) + { + database.GetChildren(beatmapSet); + beatmapSet.Beatmaps.ForEach(b => { if (b.Metadata == null) b.Metadata = beatmapSet.Metadata; }); + + return new BeatmapGroup(beatmapSet, database) + { + SelectionChanged = SelectionChanged, + StartRequested = b => StartRequested?.Invoke(), + State = BeatmapGroupState.Collapsed + }; + } + + [BackgroundDependencyLoader(permitNulls: true)] + private void load(BeatmapDatabase database) + { + this.database = database; + } + + private void addGroup(BeatmapGroup group) + { + groups.Add(group); + panels.Add(group.Header); + panels.AddRange(group.BeatmapPanels); + } + + private void removeGroup(BeatmapGroup group) { groups.Remove(group); panels.Remove(group.Header); @@ -65,18 +252,12 @@ namespace osu.Game.Screens.Select scrollableContent.Remove(group.Header); scrollableContent.Remove(group.BeatmapPanels); + if (selectedGroup == group) + SelectNext(); + computeYPositions(); } - private void movePanel(Panel panel, bool advance, bool animated, ref float currentY) - { - yPositions.Add(currentY); - panel.MoveToY(currentY, animated ? 750 : 0, EasingTypes.OutExpo); - - if (advance) - currentY += panel.DrawHeight + 5; - } - /// /// Computes the target Y positions for every panel in the carousel. /// @@ -99,7 +280,7 @@ namespace osu.Game.Screens.Select foreach (BeatmapPanel panel in group.BeatmapPanels) { - if (panel == SelectedPanel) + if (panel == selectedPanel) selectedY = currentY + panel.DrawHeight / 2 - DrawHeight / 2; panel.MoveToX(-50, 500, EasingTypes.OutExpo); @@ -129,105 +310,62 @@ namespace osu.Game.Screens.Select return selectedY; } - public void SelectBeatmap(BeatmapInfo beatmap, bool animated = true) + private void movePanel(Panel panel, bool advance, bool animated, ref float currentY) { - foreach (BeatmapGroup group in groups) - { - var panel = group.BeatmapPanels.FirstOrDefault(p => p.Beatmap.Equals(beatmap)); - if (panel != null) - { - selectGroup(group, panel, animated); - return; - } - } + yPositions.Add(currentY); + panel.MoveToY(currentY, animated ? 750 : 0, EasingTypes.OutExpo); + + if (advance) + currentY += panel.DrawHeight + 5; } - private void selectGroup(BeatmapGroup group, BeatmapPanel panel, bool animated = true) + private void selectGroup(BeatmapGroup group, BeatmapPanel panel = null, bool animated = true) { + if (panel == null) + panel = group.BeatmapPanels.First(); + Trace.Assert(group.BeatmapPanels.Contains(panel), @"Selected panel must be in provided group"); - if (SelectedGroup != null && SelectedGroup != group && SelectedGroup.State != BeatmapGroupState.Hidden) - SelectedGroup.State = BeatmapGroupState.Collapsed; + if (selectedGroup != null && selectedGroup != group && selectedGroup.State != BeatmapGroupState.Hidden) + selectedGroup.State = BeatmapGroupState.Collapsed; group.State = BeatmapGroupState.Expanded; - SelectedGroup = group; + selectedGroup = group; panel.State = PanelSelectedState.Selected; - SelectedPanel = panel; + selectedPanel = panel; float selectedY = computeYPositions(animated); ScrollTo(selectedY, animated); } - public void Sort(SortMode mode) + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) { - List sortedGroups = new List(groups); - switch (mode) + int direction = 0; + bool skipDifficulties = false; + + switch (args.Key) { - case SortMode.Artist: - sortedGroups.Sort((x, y) => string.Compare(x.BeatmapSet.Metadata.Artist, y.BeatmapSet.Metadata.Artist, StringComparison.InvariantCultureIgnoreCase)); + case Key.Up: + direction = -1; break; - case SortMode.Title: - sortedGroups.Sort((x, y) => string.Compare(x.BeatmapSet.Metadata.Title, y.BeatmapSet.Metadata.Title, StringComparison.InvariantCultureIgnoreCase)); + case Key.Down: + direction = 1; break; - case SortMode.Author: - sortedGroups.Sort((x, y) => string.Compare(x.BeatmapSet.Metadata.Author, y.BeatmapSet.Metadata.Author, StringComparison.InvariantCultureIgnoreCase)); + case Key.Left: + direction = -1; + skipDifficulties = true; break; - case SortMode.Difficulty: - sortedGroups.Sort((x, y) => x.BeatmapSet.MaxStarDifficulty.CompareTo(y.BeatmapSet.MaxStarDifficulty)); - break; - default: - Sort(SortMode.Artist); // Temporary + case Key.Right: + direction = 1; + skipDifficulties = true; break; } - scrollableContent.Clear(false); - panels.Clear(); - groups.Clear(); + if (direction == 0) + return base.OnKeyDown(state, args); - foreach (BeatmapGroup group in sortedGroups) - AddGroup(group); - } - - /// - /// Computes the x-offset of currently visible panels. Makes the carousel appear round. - /// - /// - /// Vertical distance from the center of the carousel container - /// ranging from -1 to 1. - /// - /// Half the height of the carousel container. - private static float offsetX(float dist, float halfHeight) - { - // The radius of the circle the carousel moves on. - const float circle_radius = 3; - double discriminant = Math.Max(0, circle_radius * circle_radius - dist * dist); - float x = (circle_radius - (float)Math.Sqrt(discriminant)) * halfHeight; - - return 125 + x; - } - - /// - /// Update a panel's x position and multiplicative alpha based on its y position and - /// the current scroll position. - /// - /// The panel to be updated. - /// Half the draw height of the carousel container. - private void updatePanel(Panel p, float halfHeight) - { - var height = p.IsPresent ? p.DrawHeight : 0; - - float panelDrawY = p.Position.Y - Current + height / 2; - float dist = Math.Abs(1f - panelDrawY / halfHeight); - - // Setting the origin position serves as an additive position on top of potential - // local transformation we may want to apply (e.g. when a panel gets selected, we - // may want to smoothly transform it leftwards.) - p.OriginPosition = new Vector2(-offsetX(dist, halfHeight), 0); - - // We are applying a multiplicative alpha (which is internally done by nesting an - // additional container and setting that container's alpha) such that we can - // layer transformations on top, with a similar reasoning to the previous comment. - p.SetMultiplicativeAlpha(MathHelper.Clamp(1.75f - 1.5f * dist, 0, 1)); + SelectNext(direction, skipDifficulties); + return true; } protected override void Update() @@ -276,80 +414,46 @@ namespace osu.Game.Screens.Select updatePanel(p, halfHeight); } - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + /// + /// Computes the x-offset of currently visible panels. Makes the carousel appear round. + /// + /// + /// Vertical distance from the center of the carousel container + /// ranging from -1 to 1. + /// + /// Half the height of the carousel container. + private static float offsetX(float dist, float halfHeight) { - int direction = 0; - bool skipDifficulties = false; + // The radius of the circle the carousel moves on. + const float circle_radius = 3; + double discriminant = Math.Max(0, circle_radius * circle_radius - dist * dist); + float x = (circle_radius - (float)Math.Sqrt(discriminant)) * halfHeight; - switch (args.Key) - { - case Key.Up: - direction = -1; - break; - case Key.Down: - direction = 1; - break; - case Key.Left: - direction = -1; - skipDifficulties = true; - break; - case Key.Right: - direction = 1; - skipDifficulties = true; - break; - } - - if (direction == 0) - return base.OnKeyDown(state, args); - - SelectNext(direction, skipDifficulties); - return true; + return 125 + x; } - public void SelectNext(int direction = 1, bool skipDifficulties = true) + /// + /// Update a panel's x position and multiplicative alpha based on its y position and + /// the current scroll position. + /// + /// The panel to be updated. + /// Half the draw height of the carousel container. + private void updatePanel(Panel p, float halfHeight) { - if (!skipDifficulties && SelectedGroup != null) - { - int i = SelectedGroup.BeatmapPanels.IndexOf(SelectedPanel) + direction; + var height = p.IsPresent ? p.DrawHeight : 0; - if (i >= 0 && i < SelectedGroup.BeatmapPanels.Count) - { - //changing difficulty panel, not set. - selectGroup(SelectedGroup, SelectedGroup.BeatmapPanels[i]); - return; - } - } + float panelDrawY = p.Position.Y - Current + height / 2; + float dist = Math.Abs(1f - panelDrawY / halfHeight); - int startIndex = groups.IndexOf(SelectedGroup); - int index = startIndex; + // Setting the origin position serves as an additive position on top of potential + // local transformation we may want to apply (e.g. when a panel gets selected, we + // may want to smoothly transform it leftwards.) + p.OriginPosition = new Vector2(-offsetX(dist, halfHeight), 0); - do - { - index = (index + direction + groups.Count) % groups.Count; - if (groups[index].State != BeatmapGroupState.Hidden) - { - SelectBeatmap(groups[index].BeatmapPanels.First().Beatmap); - return; - } - } while (index != startIndex); + // We are applying a multiplicative alpha (which is internally done by nesting an + // additional container and setting that container's alpha) such that we can + // layer transformations on top, with a similar reasoning to the previous comment. + p.SetMultiplicativeAlpha(MathHelper.Clamp(1.75f - 1.5f * dist, 0, 1)); } - - public void SelectRandom() - { - List visibleGroups = groups.Where(selectGroup => selectGroup.State != BeatmapGroupState.Hidden).ToList(); - if (visibleGroups.Count < 1) - return; - BeatmapGroup group = visibleGroups[RNG.Next(visibleGroups.Count)]; - BeatmapPanel panel = group?.BeatmapPanels.First(); - - if (panel == null) - return; - - selectGroup(group, panel); - } - - public IEnumerator GetEnumerator() => groups.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } } diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 2a25928dc7..6fb05b8988 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -5,6 +5,7 @@ using System; using OpenTK; using OpenTK.Graphics; using osu.Framework.Allocation; +using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; @@ -15,14 +16,13 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Select.Filter; using Container = osu.Framework.Graphics.Containers.Container; using osu.Framework.Input; +using osu.Game.Modes; namespace osu.Game.Screens.Select { public class FilterControl : Container { - public Action FilterChanged; - - public string Search => searchTextBox.Text; + public Action FilterChanged; private OsuTabControl sortTabs; @@ -30,16 +30,16 @@ namespace osu.Game.Screens.Select private SortMode sort = SortMode.Title; public SortMode Sort - { - get { return sort; } + { + get { return sort; } set { if (sort != value) { sort = value; - FilterChanged?.Invoke(); + FilterChanged?.Invoke(CreateCriteria()); } - } + } } private GroupMode group = GroupMode.All; @@ -51,11 +51,19 @@ namespace osu.Game.Screens.Select if (group != value) { group = value; - FilterChanged?.Invoke(); + FilterChanged?.Invoke(CreateCriteria()); } } } + public FilterCriteria CreateCriteria() => new FilterCriteria + { + Group = group, + Sort = sort, + SearchText = searchTextBox.Text, + Mode = playMode + }; + public Action Exit; private SearchTextBox searchTextBox; @@ -88,7 +96,7 @@ namespace osu.Game.Screens.Select OnChange = (sender, newText) => { if (newText) - FilterChanged?.Invoke(); + FilterChanged?.Invoke(CreateCriteria()); }, Exit = () => Exit?.Invoke(), }, @@ -152,16 +160,21 @@ namespace osu.Game.Screens.Select searchTextBox.HoldFocus = false; searchTextBox.TriggerFocusLost(); } - + public void Activate() { searchTextBox.HoldFocus = true; } - [BackgroundDependencyLoader] - private void load(OsuColour colours) + private readonly Bindable playMode = new Bindable(); + + [BackgroundDependencyLoader(permitNulls:true)] + private void load(OsuColour colours, OsuGame osu) { sortTabs.AccentColour = colours.GreenLight; + + if (osu != null) + playMode.BindTo(osu.PlayMode); } protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => true; diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs new file mode 100644 index 0000000000..acf0954418 --- /dev/null +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -0,0 +1,65 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Modes; +using osu.Game.Screens.Select.Filter; + +namespace osu.Game.Screens.Select +{ + public class FilterCriteria + { + public GroupMode Group; + public SortMode Sort; + public string SearchText; + public PlayMode Mode; + + public void Filter(List groups) + { + foreach (var g in groups) + { + var set = g.BeatmapSet; + + bool hasCurrentMode = set.Beatmaps.Any(bm => bm.Mode == Mode); + + bool match = hasCurrentMode; + + match &= string.IsNullOrEmpty(SearchText) + || (set.Metadata.Artist ?? string.Empty).IndexOf(SearchText, StringComparison.InvariantCultureIgnoreCase) != -1 + || (set.Metadata.ArtistUnicode ?? string.Empty).IndexOf(SearchText, StringComparison.InvariantCultureIgnoreCase) != -1 + || (set.Metadata.Title ?? string.Empty).IndexOf(SearchText, StringComparison.InvariantCultureIgnoreCase) != -1 + || (set.Metadata.TitleUnicode ?? string.Empty).IndexOf(SearchText, StringComparison.InvariantCultureIgnoreCase) != -1; + + switch (g.State) + { + case BeatmapGroupState.Hidden: + if (match) g.State = BeatmapGroupState.Collapsed; + break; + default: + if (!match) g.State = BeatmapGroupState.Hidden; + break; + } + } + + switch (Sort) + { + default: + case SortMode.Artist: + groups.Sort((x, y) => string.Compare(x.BeatmapSet.Metadata.Artist, y.BeatmapSet.Metadata.Artist, StringComparison.InvariantCultureIgnoreCase)); + break; + case SortMode.Title: + groups.Sort((x, y) => string.Compare(x.BeatmapSet.Metadata.Title, y.BeatmapSet.Metadata.Title, StringComparison.InvariantCultureIgnoreCase)); + break; + case SortMode.Author: + groups.Sort((x, y) => string.Compare(x.BeatmapSet.Metadata.Author, y.BeatmapSet.Metadata.Author, StringComparison.InvariantCultureIgnoreCase)); + break; + case SortMode.Difficulty: + groups.Sort((x, y) => x.BeatmapSet.MaxStarDifficulty.CompareTo(y.BeatmapSet.MaxStarDifficulty)); + break; + } + } + } +} \ No newline at end of file diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index a0f20242c2..60017bfbd8 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -2,10 +2,8 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using System.Collections.Generic; using System.Linq; using System.Threading; -using System.Threading.Tasks; using OpenTK; using OpenTK.Input; using osu.Framework.Allocation; @@ -19,7 +17,6 @@ using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Transforms; using osu.Framework.Input; using osu.Framework.Screens; -using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Database; @@ -38,7 +35,7 @@ namespace osu.Game.Screens.Select private BeatmapDatabase database; protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap); - private CarouselContainer carousel; + private BeatmapCarousel carousel; private TrackManager trackManager; private DialogOverlay dialogOverlay; @@ -51,8 +48,6 @@ namespace osu.Game.Screens.Select private SampleChannel sampleChangeDifficulty; private SampleChannel sampleChangeBeatmap; - private List beatmapGroups; - protected virtual bool ShowFooter => true; /// @@ -72,7 +67,6 @@ namespace osu.Game.Screens.Select const float carousel_width = 640; const float filter_height = 100; - beatmapGroups = new List(); Add(new ParallaxContainer { Padding = new MarginPadding { Top = filter_height }, @@ -87,18 +81,20 @@ namespace osu.Game.Screens.Select } } }); - Add(carousel = new CarouselContainer + Add(carousel = new BeatmapCarousel { RelativeSizeAxes = Axes.Y, Size = new Vector2(carousel_width, 1), Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, + SelectionChanged = selectionChanged, + StartRequested = raiseSelect }); Add(FilterControl = new FilterControl { RelativeSizeAxes = Axes.X, Height = filter_height, - FilterChanged = () => filterChanged(), + FilterChanged = criteria => filterChanged(criteria), Exit = Exit, }); Add(beatmapInfoWedge = new BeatmapInfoWedge @@ -132,8 +128,7 @@ namespace osu.Game.Screens.Select } [BackgroundDependencyLoader(permitNulls: true)] - private void load(BeatmapDatabase beatmaps, AudioManager audio, DialogOverlay dialog, Framework.Game game, - OsuGame osu, OsuColour colours) + private void load(BeatmapDatabase beatmaps, AudioManager audio, DialogOverlay dialog, OsuGame osu, OsuColour colours) { if (Footer != null) { @@ -161,7 +156,16 @@ namespace osu.Game.Screens.Select initialAddSetsTask = new CancellationTokenSource(); - Task.Factory.StartNew(() => addBeatmapSets(game, initialAddSetsTask.Token), initialAddSetsTask.Token); + carousel.BeatmapsChanged = beatmapsLoaded; + carousel.Beatmaps = database.Query().Where(b => !b.DeletePending); + } + + private void beatmapsLoaded() + { + if (Beatmap != null) + carousel.SelectBeatmap(Beatmap.BeatmapInfo, false); + else + carousel.SelectNext(); } private void raiseSelect() @@ -173,61 +177,15 @@ namespace osu.Game.Screens.Select } public void SelectRandom() => carousel.SelectRandom(); + protected abstract void OnSelected(); - private ScheduledDelegate filterTask; - - private void filterChanged(bool debounce = true, bool eagerSelection = true) + private void filterChanged(FilterCriteria criteria, bool debounce = true) { - filterTask?.Cancel(); - filterTask = Scheduler.AddDelayed(() => - { - filterTask = null; - var search = FilterControl.Search; - BeatmapGroup newSelection = null; - carousel.Sort(FilterControl.Sort); - foreach (var beatmapGroup in carousel) - { - var set = beatmapGroup.BeatmapSet; - - bool hasCurrentMode = set.Beatmaps.Any(bm => bm.Mode == playMode); - - bool match = hasCurrentMode; - - match &= string.IsNullOrEmpty(search) - || (set.Metadata.Artist ?? "").IndexOf(search, StringComparison.InvariantCultureIgnoreCase) != -1 - || (set.Metadata.ArtistUnicode ?? "").IndexOf(search, StringComparison.InvariantCultureIgnoreCase) != -1 - || (set.Metadata.Title ?? "").IndexOf(search, StringComparison.InvariantCultureIgnoreCase) != -1 - || (set.Metadata.TitleUnicode ?? "").IndexOf(search, StringComparison.InvariantCultureIgnoreCase) != -1; - - if (match) - { - if (newSelection == null || beatmapGroup.BeatmapSet.OnlineBeatmapSetID == Beatmap.BeatmapSetInfo.OnlineBeatmapSetID) - { - if (newSelection != null) - newSelection.State = BeatmapGroupState.Collapsed; - newSelection = beatmapGroup; - } - else - beatmapGroup.State = BeatmapGroupState.Collapsed; - } - else - { - beatmapGroup.State = BeatmapGroupState.Hidden; - } - } - - if (newSelection != null) - { - if (newSelection.BeatmapPanels.Any(b => b.Beatmap.ID == Beatmap.BeatmapInfo.ID)) - carousel.SelectBeatmap(Beatmap.BeatmapInfo, false); - else if (eagerSelection) - carousel.SelectBeatmap(newSelection.BeatmapSet.Beatmaps[0], false); - } - }, debounce ? 250 : 0); + carousel.Filter(criteria, debounce); } - private void onBeatmapSetAdded(BeatmapSetInfo s) => Schedule(() => addBeatmapSet(s, Game, true)); + private void onBeatmapSetAdded(BeatmapSetInfo s) => carousel.AddBeatmap(s); private void onBeatmapSetRemoved(BeatmapSetInfo s) => Schedule(() => removeBeatmapSet(s)); @@ -288,10 +246,7 @@ namespace osu.Game.Screens.Select initialAddSetsTask.Cancel(); } - private void playMode_ValueChanged(object sender, EventArgs e) - { - filterChanged(false); - } + private void playMode_ValueChanged(object sender, EventArgs e) => carousel.Filter(); private void changeBackground(WorkingBeatmap beatmap) { @@ -352,63 +307,18 @@ namespace osu.Game.Screens.Select } } - private void addBeatmapSet(BeatmapSetInfo beatmapSet, Framework.Game game, bool select = false) + private void selectBeatmap(BeatmapSetInfo beatmapSet = null) { - beatmapSet = database.GetWithChildren(beatmapSet.ID); - beatmapSet.Beatmaps.ForEach(b => - { - database.GetChildren(b); - if (b.Metadata == null) b.Metadata = beatmapSet.Metadata; - }); - - var group = new BeatmapGroup(beatmapSet, database) - { - SelectionChanged = selectionChanged, - StartRequested = b => raiseSelect() - }; - - //for the time being, let's completely load the difficulty panels in the background. - //this likely won't scale so well, but allows us to completely async the loading flow. - Task.WhenAll(group.BeatmapPanels.Select(panel => panel.LoadAsync(game))).ContinueWith(task => Schedule(delegate - { - beatmapGroups.Add(group); - - group.State = BeatmapGroupState.Collapsed; - carousel.AddGroup(group); - - filterChanged(false, false); - - if (Beatmap == null || select) - carousel.SelectBeatmap(beatmapSet.Beatmaps.First()); - else - carousel.SelectBeatmap(Beatmap.BeatmapInfo); - })); + carousel.SelectBeatmap(beatmapSet != null ? beatmapSet.Beatmaps.First() : Beatmap?.BeatmapInfo); } private void removeBeatmapSet(BeatmapSetInfo beatmapSet) { - var group = beatmapGroups.Find(b => b.BeatmapSet.ID == beatmapSet.ID); - if (group == null) return; - - if (carousel.SelectedGroup == group) - carousel.SelectNext(); - - beatmapGroups.Remove(group); - carousel.RemoveGroup(group); - - if (beatmapGroups.Count == 0) + carousel.RemoveBeatmap(beatmapSet); + if (carousel.SelectedBeatmap == null) Beatmap = null; } - private void addBeatmapSets(Framework.Game game, CancellationToken token) - { - foreach (var beatmapSet in database.Query().Where(b => !b.DeletePending)) - { - if (token.IsCancellationRequested) return; - addBeatmapSet(beatmapSet, game); - } - } - private void promptDelete() { if (Beatmap != null) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 8b0a5fd307..658b162bbb 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -197,7 +197,8 @@ - + +