1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-30 06:12:58 +08:00

Shuffle playback order in global playlist by default

RFC. Closes https://github.com/ppy/osu/issues/18169.

Implements the given proposal of keeping the current stable order but
adding a shuffle facility to the now playing overlay, and enabling it by
default.

There are more changes I want to make here but I'd like this to get
discussion first, because I am likely to continue putting this sort of
selection logic into `MusicController` and I just want to confirm nobody
is going to have a problem with that.

In particular this is not sharing the randomisation implementation with
beatmap carousel because it doesn't generalise nicely (song select cares
about the particular *beatmap difficulties* selected to rewind properly,
while the music controller only cares about picking a *beatmap set*).
This commit is contained in:
Bartłomiej Dach 2024-09-18 13:51:45 +02:00
parent cf9f8c7f66
commit 12bd516a57
No known key found for this signature in database
3 changed files with 94 additions and 3 deletions

View File

@ -34,6 +34,8 @@ namespace osu.Game.Tests.Visual.Menus
{
Queue<(IWorkingBeatmap working, TrackChangeDirection changeDirection)> trackChangeQueue = null!;
AddStep("disable shuffle", () => Game.MusicController.Shuffle.Value = false);
// ensure we have at least two beatmaps available to identify the direction the music controller navigated to.
AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(TestResources.CreateTestBeatmapSetInfo()), 5);

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@ -13,8 +14,10 @@ using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Audio.Effects;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Rulesets.Mods;
@ -43,6 +46,8 @@ namespace osu.Game.Overlays
/// </summary>
public readonly BindableBool AllowTrackControl = new BindableBool(true);
public readonly BindableBool Shuffle = new BindableBool(true);
/// <summary>
/// Fired when the global <see cref="WorkingBeatmap"/> has changed.
/// Includes direction information for display purposes.
@ -66,12 +71,18 @@ namespace osu.Game.Overlays
private AudioFilter audioDuckFilter = null!;
private readonly Bindable<RandomSelectAlgorithm> randomSelectAlgorithm = new Bindable<RandomSelectAlgorithm>();
private readonly List<BeatmapSetInfo> previousRandomSets = new List<BeatmapSetInfo>();
private int randomHistoryDirection;
[BackgroundDependencyLoader]
private void load(AudioManager audio)
private void load(AudioManager audio, OsuConfigManager configManager)
{
AddInternal(audioDuckFilter = new AudioFilter(audio.TrackMixer));
audio.Tracks.AddAdjustment(AdjustableProperty.Volume, audioDuckVolume);
sampleVolume = audio.VolumeSample.GetBoundCopy();
configManager.BindWith(OsuSetting.RandomSelectAlgorithm, randomSelectAlgorithm);
}
protected override void LoadComplete()
@ -238,8 +249,15 @@ namespace osu.Game.Overlays
queuedDirection = TrackChangeDirection.Prev;
var playableSet = getBeatmapSets().AsEnumerable().TakeWhile(i => !i.Equals(current?.BeatmapSetInfo)).LastOrDefault(s => !s.Protected || allowProtectedTracks)
BeatmapSetInfo? playableSet;
if (Shuffle.Value)
playableSet = getNextRandom(-1, allowProtectedTracks);
else
{
playableSet = getBeatmapSets().AsEnumerable().TakeWhile(i => !i.Equals(current?.BeatmapSetInfo)).LastOrDefault(s => !s.Protected || allowProtectedTracks)
?? getBeatmapSets().AsEnumerable().LastOrDefault(s => !s.Protected || allowProtectedTracks);
}
if (playableSet != null)
{
@ -327,8 +345,15 @@ namespace osu.Game.Overlays
queuedDirection = TrackChangeDirection.Next;
var playableSet = getBeatmapSets().AsEnumerable().SkipWhile(i => !i.Equals(current?.BeatmapSetInfo) || (i.Protected && !allowProtectedTracks)).ElementAtOrDefault(1)
BeatmapSetInfo? playableSet;
if (Shuffle.Value)
playableSet = getNextRandom(1, allowProtectedTracks);
else
{
playableSet = getBeatmapSets().AsEnumerable().SkipWhile(i => !i.Equals(current?.BeatmapSetInfo) || (i.Protected && !allowProtectedTracks)).ElementAtOrDefault(1)
?? getBeatmapSets().AsEnumerable().FirstOrDefault(i => !i.Protected || allowProtectedTracks);
}
var playableBeatmap = playableSet?.Beatmaps.FirstOrDefault();
@ -342,6 +367,58 @@ namespace osu.Game.Overlays
return false;
}
private BeatmapSetInfo? getNextRandom(int direction, bool allowProtectedTracks)
{
BeatmapSetInfo result;
var possibleSets = getBeatmapSets().AsEnumerable().Where(s => !s.Protected || allowProtectedTracks).ToArray();
if (possibleSets.Length == 0)
return null;
// condition below checks if the signs of `randomHistoryDirection` and `direction` are opposite and not zero.
// if that is the case, it means that the user had previously chosen next track `randomHistoryDirection` times and wants to go back,
// or that the user had previously chosen previous track `randomHistoryDirection` times and wants to go forward.
// in both cases, it means that we have a history of previous random selections that we can rewind.
if (randomHistoryDirection * direction < 0)
{
Debug.Assert(Math.Abs(randomHistoryDirection) == previousRandomSets.Count);
result = previousRandomSets[^1];
previousRandomSets.RemoveAt(previousRandomSets.Count - 1);
randomHistoryDirection += direction;
return result;
}
// if the early-return above didn't cover it, it means that we have no history to fall back on
// and need to actually choose something random.
switch (randomSelectAlgorithm.Value)
{
case RandomSelectAlgorithm.Random:
result = possibleSets[RNG.Next(possibleSets.Length)];
break;
case RandomSelectAlgorithm.RandomPermutation:
var notYetPlayedSets = possibleSets.Except(previousRandomSets).ToArray();
if (notYetPlayedSets.Length == 0)
{
notYetPlayedSets = possibleSets;
previousRandomSets.Clear();
randomHistoryDirection = 0;
}
result = notYetPlayedSets[RNG.Next(notYetPlayedSets.Length)];
break;
default:
throw new ArgumentOutOfRangeException(nameof(randomSelectAlgorithm), randomSelectAlgorithm.Value, "Unsupported random select algorithm");
}
previousRandomSets.Add(result);
randomHistoryDirection += direction;
return result;
}
private void restartTrack()
{
// if not scheduled, the previously track will be stopped one frame later (see ScheduleAfterChildren logic in GameBase).

View File

@ -47,6 +47,7 @@ namespace osu.Game.Overlays
private IconButton prevButton = null!;
private IconButton playButton = null!;
private IconButton nextButton = null!;
private MusicIconButton shuffleButton = null!;
private IconButton playlistButton = null!;
private ScrollingTextContainer title = null!, artist = null!;
@ -69,6 +70,7 @@ namespace osu.Game.Overlays
private OsuColour colours { get; set; } = null!;
private Bindable<bool> allowTrackControl = null!;
private BindableBool shuffle = new BindableBool(true);
public NowPlayingOverlay()
{
@ -162,6 +164,13 @@ namespace osu.Game.Overlays
Action = () => musicController.NextTrack(),
Icon = FontAwesome.Solid.StepForward,
},
shuffleButton = new MusicIconButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Action = shuffle.Toggle,
Icon = FontAwesome.Solid.Random,
}
}
},
playlistButton = new MusicIconButton
@ -227,6 +236,9 @@ namespace osu.Game.Overlays
allowTrackControl = musicController.AllowTrackControl.GetBoundCopy();
allowTrackControl.BindValueChanged(_ => Scheduler.AddOnce(updateEnabledStates), true);
shuffle.BindTo(musicController.Shuffle);
shuffle.BindValueChanged(s => shuffleButton.FadeColour(s.NewValue ? colours.Yellow : Color4.White, 200, Easing.OutQuint), true);
musicController.TrackChanged += trackChanged;
trackChanged(beatmap.Value);
}