1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-16 00:45:43 +08:00

Merge pull request #504 from peppy/song-select-loading-reoptimisation

Song select performance / code cleanup.
This commit is contained in:
Dan Balasescu 2017-03-18 02:19:02 +09:00 committed by GitHub
commit 124efe8126
13 changed files with 452 additions and 352 deletions

View File

@ -10,7 +10,7 @@ using osu.Game.Database;
namespace osu.Game.Beatmaps.Drawables
{
internal class BeatmapGroup : IStateful<BeatmapGroupState>
public class BeatmapGroup : IStateful<BeatmapGroupState>
{
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)
{

View File

@ -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),
}
}

View File

@ -17,7 +17,7 @@ using OpenTK.Graphics;
namespace osu.Game.Beatmaps.Drawables
{
internal class BeatmapSetHeader : Panel
public class BeatmapSetHeader : Panel
{
public Action<BeatmapSetHeader> 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<bool>(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<BeatmapPanel> panels)

View File

@ -12,7 +12,7 @@ using osu.Framework.Extensions.Color4Extensions;
namespace osu.Game.Beatmaps.Drawables
{
internal class Panel : Container, IStateful<PanelSelectedState>
public class Panel : Container, IStateful<PanelSelectedState>
{
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,

View File

@ -123,16 +123,15 @@ namespace osu.Game.Database
/// <param name="paths">Multiple locations on disk</param>
public void Import(IEnumerable<string> paths)
{
Stack<BeatmapSetInfo> sets = new Stack<BeatmapSetInfo>();
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);
}
}
/// <summary>
@ -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;

View File

@ -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)
{

View File

@ -24,7 +24,7 @@ namespace osu.Game.Database
[OneToMany(CascadeOperations = CascadeOperation.All)]
public List<BeatmapInfo> Beatmaps { get; set; }
public float MaxStarDifficulty => Beatmaps.Max(b => b.StarDifficulty);
public double MaxStarDifficulty => Beatmaps.Max(b => b.StarDifficulty);
public bool DeletePending { get; set; }

View File

@ -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;

View File

@ -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<BeatmapGroup>
internal class BeatmapCarousel : ScrollContainer, IEnumerable<BeatmapGroup>
{
private Container<Panel> scrollableContent;
private List<BeatmapGroup> groups = new List<BeatmapGroup>();
private List<Panel> panels = new List<Panel>();
public BeatmapInfo SelectedBeatmap => selectedPanel?.Beatmap;
public BeatmapGroup SelectedGroup { get; private set; }
public BeatmapPanel SelectedPanel { get; private set; }
public Action BeatmapsChanged;
public IEnumerable<BeatmapSetInfo> Beatmaps
{
get
{
return groups.Select(g => g.BeatmapSet);
}
set
{
scrollableContent.Clear(false);
panels.Clear();
groups.Clear();
List<BeatmapGroup> 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<float> yPositions = new List<float>();
public CarouselContainer()
{
DistanceDecayJump = 0.01;
/// <summary>
/// Required for now unfortunately.
/// </summary>
private BeatmapDatabase database;
private Container<Panel> scrollableContent;
private List<BeatmapGroup> groups = new List<BeatmapGroup>();
private List<Panel> panels = new List<Panel>();
private BeatmapGroup selectedGroup;
private BeatmapPanel selectedPanel;
public BeatmapCarousel()
{
Add(scrollableContent = new Container<Panel>
{
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<BeatmapGroup, BeatmapInfo> 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<BeatmapGroup> 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<BeatmapGroup>(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<BeatmapGroup> 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;
}
/// <summary>
/// Computes the target Y positions for every panel in the carousel.
/// </summary>
@ -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<BeatmapGroup> sortedGroups = new List<BeatmapGroup>(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);
}
/// <summary>
/// Computes the x-offset of currently visible panels. Makes the carousel appear round.
/// </summary>
/// <param name="dist">
/// Vertical distance from the center of the carousel container
/// ranging from -1 to 1.
/// </param>
/// <param name="halfHeight">Half the height of the carousel container.</param>
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;
}
/// <summary>
/// Update a panel's x position and multiplicative alpha based on its y position and
/// the current scroll position.
/// </summary>
/// <param name="p">The panel to be updated.</param>
/// <param name="halfHeight">Half the draw height of the carousel container.</param>
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)
/// <summary>
/// Computes the x-offset of currently visible panels. Makes the carousel appear round.
/// </summary>
/// <param name="dist">
/// Vertical distance from the center of the carousel container
/// ranging from -1 to 1.
/// </param>
/// <param name="halfHeight">Half the height of the carousel container.</param>
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)
/// <summary>
/// Update a panel's x position and multiplicative alpha based on its y position and
/// the current scroll position.
/// </summary>
/// <param name="p">The panel to be updated.</param>
/// <param name="halfHeight">Half the draw height of the carousel container.</param>
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<BeatmapGroup> 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<BeatmapGroup> GetEnumerator() => groups.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

View File

@ -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<FilterCriteria> FilterChanged;
private OsuTabControl<SortMode> 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> playMode = new Bindable<PlayMode>();
[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;

View File

@ -0,0 +1,65 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// 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<BeatmapGroup> 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;
}
}
}
}

View File

@ -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<BeatmapGroup> beatmapGroups;
protected virtual bool ShowFooter => true;
/// <summary>
@ -72,7 +67,6 @@ namespace osu.Game.Screens.Select
const float carousel_width = 640;
const float filter_height = 100;
beatmapGroups = new List<BeatmapGroup>();
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<BeatmapSetInfo>().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<BeatmapSetInfo>(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<BeatmapSetInfo>().Where(b => !b.DeletePending))
{
if (token.IsCancellationRequested) return;
addBeatmapSet(beatmapSet, game);
}
}
private void promptDelete()
{
if (Beatmap != null)

View File

@ -197,7 +197,8 @@
<Compile Include="Screens\Play\PlayerLoader.cs" />
<Compile Include="Screens\Play\SkipButton.cs" />
<Compile Include="Modes\UI\StandardComboCounter.cs" />
<Compile Include="Screens\Select\CarouselContainer.cs" />
<Compile Include="Screens\Select\BeatmapCarousel.cs" />
<Compile Include="Screens\Select\FilterCriteria.cs" />
<Compile Include="Screens\Select\Filter\GroupMode.cs" />
<Compile Include="Screens\Select\Filter\SortMode.cs" />
<Compile Include="Screens\Select\MatchSongSelect.cs" />