diff --git a/osu.Desktop.VisualTests/Tests/TestCaseMusicController.cs b/osu.Desktop.VisualTests/Tests/TestCaseMusicController.cs new file mode 100644 index 0000000000..070745a49c --- /dev/null +++ b/osu.Desktop.VisualTests/Tests/TestCaseMusicController.cs @@ -0,0 +1,52 @@ +//Copyright (c) 2007-2016 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 System.Text; +using System.Threading.Tasks; +using osu.Framework.Graphics; +using osu.Framework.GameModes.Testing; +using osu.Game.Overlays; +using osu.Framework.Timing; +using osu.Framework; + +namespace osu.Desktop.Tests +{ + class TestCaseMusicController : TestCase + { + public override string Name => @"Music Controller"; + public override string Description => @"Tests music controller ui."; + + IFrameBasedClock ourClock; + protected override IFrameBasedClock Clock => ourClock; + + protected MusicController mc; + + protected override void Load(BaseGame game) + { + base.Load(game); + ourClock = new FramedClock(); + } + + public override void Reset() + { + base.Reset(); + ourClock.ProcessFrame(); + mc = new MusicController + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre + }; + Add(mc); + AddToggle(@"Show", mc.ToggleVisibility); + } + + protected override void Update() + { + base.Update(); + ourClock.ProcessFrame(); + } + } +} diff --git a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj index 9e23ee97b9..e8f8656f15 100644 --- a/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj +++ b/osu.Desktop.VisualTests/osu.Desktop.VisualTests.csproj @@ -158,6 +158,7 @@ + diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 417e289b65..63a33c700e 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -120,7 +120,7 @@ namespace osu.Game.Beatmaps public void TransferTo(WorkingBeatmap working) { - if (track != null && working.BeatmapInfo.Metadata.AudioFile == BeatmapInfo.Metadata.AudioFile && working.BeatmapInfo.BeatmapSet.Path == BeatmapInfo.BeatmapSet.Path) + if (track != null && BeatmapInfo.AudioEquals(working.BeatmapInfo)) working.track = track; } } diff --git a/osu.Game/Database/BeatmapInfo.cs b/osu.Game/Database/BeatmapInfo.cs index 4d18976ef0..14bfcb337e 100644 --- a/osu.Game/Database/BeatmapInfo.cs +++ b/osu.Game/Database/BeatmapInfo.cs @@ -1,73 +1,77 @@ -using System; -using System.Linq; -using osu.Game.Beatmaps.Samples; -using osu.Game.GameModes.Play; -using SQLite.Net.Attributes; -using SQLiteNetExtensions.Attributes; - -namespace osu.Game.Database -{ - public class BeatmapInfo : IEquatable - { - [PrimaryKey] +using System; +using System.Linq; +using osu.Game.Beatmaps.Samples; +using osu.Game.GameModes.Play; +using SQLite.Net.Attributes; +using SQLiteNetExtensions.Attributes; + +namespace osu.Game.Database +{ + public class BeatmapInfo : IEquatable + { + [PrimaryKey] public int BeatmapID { get; set; } - [ForeignKey(typeof(BeatmapSetInfo))] - public int BeatmapSetID { get; set; } - - [ManyToOne] - public BeatmapSetInfo BeatmapSet { get; set; } - - [ForeignKey(typeof(BeatmapMetadata))] - public int BeatmapMetadataID { get; set; } - - [OneToOne(CascadeOperations = CascadeOperation.All)] + [ForeignKey(typeof(BeatmapSetInfo))] + public int BeatmapSetID { get; set; } + + [ManyToOne] + public BeatmapSetInfo BeatmapSet { get; set; } + + [ForeignKey(typeof(BeatmapMetadata))] + public int BeatmapMetadataID { get; set; } + + [OneToOne(CascadeOperations = CascadeOperation.All)] public BeatmapMetadata Metadata { get; set; } - [ForeignKey(typeof(BaseDifficulty)), NotNull] - public int BaseDifficultyID { get; set; } - - [OneToOne(CascadeOperations = CascadeOperation.All)] - public BaseDifficulty BaseDifficulty { get; set; } - - public string Path { get; set; } - - // General - public int AudioLeadIn { get; set; } - public bool Countdown { get; set; } - public SampleSet SampleSet { get; set; } - public float StackLeniency { get; set; } - public bool SpecialStyle { get; set; } - public PlayMode Mode { get; set; } - public bool LetterboxInBreaks { get; set; } - public bool WidescreenStoryboard { get; set; } - - // Editor - // This bookmarks stuff is necessary because DB doesn't know how to store int[] - public string StoredBookmarks { get; internal set; } - [Ignore] - public int[] Bookmarks - { - get - { - return StoredBookmarks.Split(',').Select(b => int.Parse(b)).ToArray(); - } - set - { - StoredBookmarks = string.Join(",", value); - } - } - public double DistanceSpacing { get; set; } - public int BeatDivisor { get; set; } - public int GridSize { get; set; } - public double TimelineZoom { get; set; } - - // Metadata - public string Version { get; set; } - - public bool Equals(BeatmapInfo other) - { - return BeatmapID == other?.BeatmapID; - } - } -} \ No newline at end of file + [ForeignKey(typeof(BaseDifficulty)), NotNull] + public int BaseDifficultyID { get; set; } + + [OneToOne(CascadeOperations = CascadeOperation.All)] + public BaseDifficulty BaseDifficulty { get; set; } + + public string Path { get; set; } + + // General + public int AudioLeadIn { get; set; } + public bool Countdown { get; set; } + public SampleSet SampleSet { get; set; } + public float StackLeniency { get; set; } + public bool SpecialStyle { get; set; } + public PlayMode Mode { get; set; } + public bool LetterboxInBreaks { get; set; } + public bool WidescreenStoryboard { get; set; } + + // Editor + // This bookmarks stuff is necessary because DB doesn't know how to store int[] + public string StoredBookmarks { get; internal set; } + [Ignore] + public int[] Bookmarks + { + get + { + return StoredBookmarks.Split(',').Select(b => int.Parse(b)).ToArray(); + } + set + { + StoredBookmarks = string.Join(",", value); + } + } + public double DistanceSpacing { get; set; } + public int BeatDivisor { get; set; } + public int GridSize { get; set; } + public double TimelineZoom { get; set; } + + // Metadata + public string Version { get; set; } + + public bool Equals(BeatmapInfo other) + { + return BeatmapID == other?.BeatmapID; + } + + public bool AudioEquals(BeatmapInfo other) => other != null && + BeatmapSet.Path == other.BeatmapSet.Path && + (Metadata ?? BeatmapSet.Metadata).AudioFile == (other.Metadata ?? other.BeatmapSet.Metadata).AudioFile; + } +} diff --git a/osu.Game/GameModes/OsuGameMode.cs b/osu.Game/GameModes/OsuGameMode.cs index 3ba60f53f6..ea9bfcfc1b 100644 --- a/osu.Game/GameModes/OsuGameMode.cs +++ b/osu.Game/GameModes/OsuGameMode.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Linq; using System.Text; using System.Threading.Tasks; +using osu.Framework; using osu.Framework.Configuration; using osu.Framework.GameModes; using osu.Framework.Graphics.Containers; @@ -74,6 +75,12 @@ namespace osu.Game.GameModes OnBeatmapChanged(beatmap.Value); } + protected override void Load(BaseGame game) + { + base.Load(game); + beatmap = (game as OsuGameBase)?.Beatmap; + } + public override bool Push(GameMode mode) { OsuGameMode nextOsu = mode as OsuGameMode; diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 7697965cda..a462c4f0fe 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -29,6 +29,8 @@ namespace osu.Game private ChatConsole chat; + private MusicController musicController; + private MainMenu mainMenu => modeStack?.ChildGameMode as MainMenu; private Intro intro => modeStack as Intro; @@ -111,6 +113,7 @@ namespace osu.Game //overlay elements (chat = new ChatConsole(API) { Depth = 0 }).Preload(game, overlayContent.Add); + (musicController = new MusicController()).Preload(game, overlayContent.Add); (Options = new OptionsOverlay { Depth = 1 }).Preload(game, overlayContent.Add); (Toolbar = new Toolbar { @@ -118,6 +121,7 @@ namespace osu.Game OnHome = delegate { mainMenu?.MakeCurrent(); }, OnSettings = Options.ToggleVisibility, OnPlayModeChange = delegate (PlayMode m) { PlayMode.Value = m; }, + OnMusicController = musicController.ToggleVisibility }).Preload(game, t => { PlayMode.ValueChanged += delegate { Toolbar.SetGameMode(PlayMode.Value); }; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 3142446635..30a109c597 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -51,7 +51,6 @@ namespace osu.Game private void Beatmap_ValueChanged(object sender, EventArgs e) { - throw new NotImplementedException(); } protected override void Load(BaseGame game) diff --git a/osu.Game/Overlays/DragBar.cs b/osu.Game/Overlays/DragBar.cs new file mode 100644 index 0000000000..ee54d21d74 --- /dev/null +++ b/osu.Game/Overlays/DragBar.cs @@ -0,0 +1,69 @@ +//Copyright (c) 2007-2016 ppy Pty Ltd . +//Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; + +namespace osu.Game.Overlays +{ + public class DragBar : Container + { + private Box fill; + + public Action SeekRequested; + private bool isDragging; + + public DragBar() + { + RelativeSizeAxes = Axes.X; + + Children = new Drawable[] + { + fill = new Box() + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Width = 0 + } + }; + } + + public void UpdatePosition(float position) + { + if (isDragging) return; + + fill.Width = position; + } + + private void seek(InputState state) + { + float seekLocation = state.Mouse.Position.X / DrawWidth; + SeekRequested?.Invoke(seekLocation); + fill.Width = seekLocation; + } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + seek(state); + return true; + } + + protected override bool OnDrag(InputState state) + { + seek(state); + return true; + } + + protected override bool OnDragStart(InputState state) => isDragging = true; + + protected override bool OnDragEnd(InputState state) + { + isDragging = false; + return true; + } + } +} diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs new file mode 100644 index 0000000000..310bae37e4 --- /dev/null +++ b/osu.Game/Overlays/MusicController.cs @@ -0,0 +1,339 @@ +//Copyright (c) 2007-2016 ppy Pty Ltd . +//Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework; +using osu.Framework.Audio.Track; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Graphics.Transformations; +using osu.Framework.Input; +using osu.Framework.MathUtils; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Graphics; + +namespace osu.Game.Overlays +{ + public class MusicController : OverlayContainer + { + private Sprite backgroundSprite; + private DragBar progress; + private TextAwesome playButton, listButton; + private SpriteText title, artist; + private Texture fallbackTexture; + + private List playList; + private List playHistory = new List(); + private int playListIndex; + private int playHistoryIndex = -1; + + private TrackManager trackManager; + private BeatmapDatabase database; + private Bindable beatmapSource; + private WorkingBeatmap current; + + public MusicController(BeatmapDatabase db = null) + { + database = db; + Width = 400; + Height = 130; + CornerRadius = 5; + Masking = true; + Anchor = Anchor.TopRight;//placeholder + Origin = Anchor.TopRight; + Position = new Vector2(10, 60); + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = new Color4(0, 0, 0, 127) + }, + title = new SpriteText + { + Origin = Anchor.BottomCentre, + Anchor = Anchor.TopCentre, + Position = new Vector2(0, 40), + TextSize = 25, + Colour = Color4.White, + Text = @"Nothing to play", + Font = @"Exo2.0-MediumItalic" + }, + artist = new SpriteText + { + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + Position = new Vector2(0, 45), + TextSize = 15, + Colour = Color4.White, + Text = @"Nothing to play", + Font = @"Exo2.0-BoldItalic" + }, + new Box + { + RelativeSizeAxes = Axes.X, + Height = 50, + Origin = Anchor.BottomCentre, + Anchor = Anchor.BottomCentre, + Colour = new Color4(0, 0, 0, 127) + }, + new ClickableContainer + { + AutoSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.BottomCentre, + Position = new Vector2(0, 30), + Action = () => + { + if (current?.Track == null) return; + if (current.Track.IsRunning) + current.Track.Stop(); + else + current.Track.Start(); + }, + Children = new Drawable[] + { + playButton = new TextAwesome + { + TextSize = 30, + Icon = FontAwesome.fa_play_circle_o, + Origin = Anchor.Centre, + Anchor = Anchor.Centre + } + } + }, + new ClickableContainer + { + AutoSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.BottomCentre, + Position = new Vector2(-30, 30), + Action = prev, + Children = new Drawable[] + { + new TextAwesome + { + TextSize = 15, + Icon = FontAwesome.fa_step_backward, + Origin = Anchor.Centre, + Anchor = Anchor.Centre + } + } + }, + new ClickableContainer + { + AutoSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.BottomCentre, + Position = new Vector2(30, 30), + Action = next, + Children = new Drawable[] + { + new TextAwesome + { + TextSize = 15, + Icon = FontAwesome.fa_step_forward, + Origin = Anchor.Centre, + Anchor = Anchor.Centre + } + } + }, + new ClickableContainer + { + AutoSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.BottomRight, + Position = new Vector2(20, 30), + Children = new Drawable[] + { + listButton = new TextAwesome + { + TextSize = 15, + Icon = FontAwesome.fa_bars, + Origin = Anchor.Centre, + Anchor = Anchor.Centre + } + } + }, + progress = new DragBar + { + Origin = Anchor.BottomCentre, + Anchor = Anchor.BottomCentre, + Height = 10, + Colour = Color4.Orange, + SeekRequested = seek + } + }; + } + + protected override void Load(BaseGame game) + { + base.Load(game); + var osuGame = game as OsuGameBase; + + if (osuGame != null) + { + if (database == null) database = osuGame.Beatmaps; + trackManager = osuGame.Audio.Track; + } + + beatmapSource = osuGame?.Beatmap ?? new Bindable(); + playList = database.GetAllWithChildren(); + + backgroundSprite = getScaledSprite(fallbackTexture = game.Textures.Get(@"Backgrounds/bg4")); + AddInternal(backgroundSprite); + } + + protected override void LoadComplete() + { + beatmapSource.ValueChanged += workingChanged; + workingChanged(); + base.LoadComplete(); + } + + protected override void Update() + { + base.Update(); + if (current?.Track == null) return; + + progress.UpdatePosition((float)(current.Track.CurrentTime / current.Track.Length)); + playButton.Icon = current.Track.IsRunning ? FontAwesome.fa_pause_circle_o : FontAwesome.fa_play_circle_o; + + if (current.Track.HasCompleted && !current.Track.Looping) next(); + } + + private void workingChanged(object sender = null, EventArgs e = null) + { + if (beatmapSource.Value == current) return; + bool audioEquals = current?.BeatmapInfo.AudioEquals(beatmapSource.Value.BeatmapInfo) ?? false; + current = beatmapSource.Value; + updateDisplay(current, audioEquals ? null : (bool?)true); + appendToHistory(current.BeatmapInfo); + } + + private void appendToHistory(BeatmapInfo beatmap) + { + if (playHistoryIndex >= 0) + { + if (beatmap.AudioEquals(playHistory[playHistoryIndex])) + return; + if (playHistoryIndex < playHistory.Count - 1) + playHistory.RemoveRange(playHistoryIndex + 1, playHistory.Count - playHistoryIndex - 1); + } + playHistory.Insert(++playHistoryIndex, beatmap); + } + + private void prev() + { + if (playHistoryIndex > 0) + play(playHistory[--playHistoryIndex], false); + } + + private void next() + { + if (playHistoryIndex < playHistory.Count - 1) + play(playHistory[++playHistoryIndex], true); + else + { + if (playList.Count == 0) return; + if (current != null && playList.Count == 1) return; + //shuffle + BeatmapInfo nextToPlay; + do + { + int j = RNG.Next(playListIndex, playList.Count); + if (j != playListIndex) + { + BeatmapSetInfo temp = playList[playListIndex]; + playList[playListIndex] = playList[j]; + playList[j] = temp; + } + + nextToPlay = playList[playListIndex++].Beatmaps[0]; + if (playListIndex == playList.Count) playListIndex = 0; + } + while (nextToPlay.AudioEquals(current?.BeatmapInfo)); + + play(nextToPlay, true); + appendToHistory(nextToPlay); + } + } + + private void play(BeatmapInfo info, bool isNext) + { + current = database.GetWorkingBeatmap(info, current); + Task.Run(() => + { + trackManager.SetExclusive(current.Track); + current.Track.Start(); + beatmapSource.Value = current; + }); + updateDisplay(current, isNext); + } + + private void updateDisplay(WorkingBeatmap beatmap, bool? isNext) + { + BeatmapMetadata metadata = beatmap.Beatmap.Metadata; + title.Text = metadata.TitleUnicode ?? metadata.Title; + artist.Text = metadata.ArtistUnicode ?? metadata.Artist; + + Sprite newBackground = getScaledSprite(beatmap.Background ?? fallbackTexture); + + Add(newBackground); + + if (isNext == true) + { + newBackground.Position = new Vector2(400, 0); + newBackground.MoveToX(0, 500, EasingTypes.OutCubic); + backgroundSprite.MoveToX(-400, 500, EasingTypes.OutCubic); + } + else if (isNext == false) + { + newBackground.Position = new Vector2(-400, 0); + newBackground.MoveToX(0, 500, EasingTypes.OutCubic); + backgroundSprite.MoveToX(400, 500, EasingTypes.OutCubic); + } + backgroundSprite.Expire(); + + backgroundSprite = newBackground; + } + + private Sprite getScaledSprite(Texture background) + { + Sprite scaledSprite = new Sprite + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Texture = background, + Depth = float.MinValue + }; + scaledSprite.Scale = new Vector2(Math.Max(DrawSize.X / scaledSprite.DrawSize.X, DrawSize.Y / scaledSprite.DrawSize.Y)); + return scaledSprite; + } + + private void seek(float position) + { + current?.Track?.Seek(current.Track.Length * position); + current?.Track?.Start(); + } + + protected override bool OnClick(InputState state) => true; + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => true; + + protected override bool OnDragStart(InputState state) => true; + + //placeholder for toggling + protected override void PopIn() => FadeIn(100); + + protected override void PopOut() => FadeOut(100); + } +} diff --git a/osu.Game/Overlays/Options/CheckBoxOption.cs b/osu.Game/Overlays/Options/CheckBoxOption.cs index 3958f65be5..4d505ca625 100644 --- a/osu.Game/Overlays/Options/CheckBoxOption.cs +++ b/osu.Game/Overlays/Options/CheckBoxOption.cs @@ -1,53 +1,54 @@ -using System; +using System; using osu.Framework.Configuration; using osu.Framework.Graphics.UserInterface; -namespace osu.Game.Overlays.Options -{ - public class CheckBoxOption : BasicCheckBox - { - private Bindable bindable; - - public Bindable Bindable - { - set - { - if (bindable != null) - bindable.ValueChanged -= bindableValueChanged; - bindable = value; - if (bindable != null) +namespace osu.Game.Overlays.Options +{ + public class CheckBoxOption : BasicCheckBox + { + private Bindable bindable; + + public Bindable Bindable + { + set + { + if (bindable != null) + bindable.ValueChanged -= bindableValueChanged; + bindable = value; + if (bindable != null) { bool state = State == CheckBoxState.Checked; if (state != bindable.Value) State = bindable.Value ? CheckBoxState.Checked : CheckBoxState.Unchecked; - bindable.ValueChanged += bindableValueChanged; - } - } + bindable.ValueChanged += bindableValueChanged; + } + } } - private void bindableValueChanged(object sender, EventArgs e) - { - State = bindable.Value ? CheckBoxState.Checked : CheckBoxState.Unchecked; - } - + + private void bindableValueChanged(object sender, EventArgs e) + { + State = bindable.Value ? CheckBoxState.Checked : CheckBoxState.Unchecked; + } + protected override void Dispose(bool isDisposing) - { - if (bindable != null) + { + if (bindable != null) bindable.ValueChanged -= bindableValueChanged; base.Dispose(isDisposing); - } - + } + protected override void OnChecked() - { - if (bindable != null) + { + if (bindable != null) bindable.Value = true; base.OnChecked(); - } - - protected override void OnUnchecked() - { - if (bindable != null) - bindable.Value = false; - base.OnChecked(); - } - } -} \ No newline at end of file + } + + protected override void OnUnchecked() + { + if (bindable != null) + bindable.Value = false; + base.OnUnchecked(); + } + } +} diff --git a/osu.Game/Overlays/Toolbar.cs b/osu.Game/Overlays/Toolbar.cs index 2931bf55d1..f4c5d356dd 100644 --- a/osu.Game/Overlays/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar.cs @@ -22,6 +22,7 @@ namespace osu.Game.Overlays public Action OnSettings; public Action OnHome; public Action OnPlayModeChange; + public Action OnMusicController; private ToolbarModeSelector modeSelector; private ToolbarButton userButton; @@ -85,6 +86,11 @@ namespace osu.Game.Overlays AutoSizeAxes = Axes.X, Children = new [] { + new ToolbarButton + { + Icon = FontAwesome.fa_music, + Action = () => OnMusicController?.Invoke() + }, new ToolbarButton { Icon = FontAwesome.fa_search diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 3b5ea8de73..4977317f0c 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -63,6 +63,8 @@ + +