1
0
mirror of https://github.com/ppy/osu.git synced 2026-06-08 17:03:58 +08:00

Compare commits

...

21 Commits

10 changed files with 228 additions and 102 deletions
@@ -8,6 +8,7 @@ using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@@ -30,7 +31,12 @@ namespace osu.Game.Tests.Visual.SongSelectV2
RemoveAllBeatmaps();
CreateCarousel();
WaitForFiltering();
AddBeatmaps(1, 3);
AddStep("add beatmap", () =>
{
var beatmap = CreateTestBeatmapSetInfo(3, false);
Realm.Write(r => r.Add(beatmap, update: true));
BeatmapSets.Add(beatmap.Detach());
});
WaitForFiltering();
AddStep("generate and add test beatmap", () =>
{
@@ -44,7 +50,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
foreach (var b in baseTestBeatmap.Beatmaps)
b.Metadata = metadata;
BeatmapSets.Add(baseTestBeatmap);
Realm.Write(r => r.Add(baseTestBeatmap, update: true));
BeatmapSets.Add(baseTestBeatmap.Detach());
});
WaitForFiltering();
@@ -269,14 +277,17 @@ namespace osu.Game.Tests.Visual.SongSelectV2
updateBeatmap(null, bs =>
{
string selectedName = bs.Beatmaps[0].DifficultyName;
Realm.Write(r => r.Remove(r.Find<BeatmapInfo>(bs.Beatmaps[0].ID)!));
bs.Beatmaps.RemoveAt(0);
var newBeatmap = createBeatmap(bs);
newBeatmap.ID = Guid.NewGuid();
newBeatmap.DifficultyName = selectedName;
newBeatmap.OnlineID = -1;
bs.Beatmaps.Add(newBeatmap);
newBeatmap = createBeatmap(bs);
newBeatmap.ID = Guid.NewGuid();
newBeatmap.DifficultyName = selectedName;
newBeatmap.OnlineID = -1;
bs.Beatmaps.Add(newBeatmap);
@@ -284,8 +295,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
WaitForFiltering();
AddAssert("selection is updateable beatmap", () => Carousel.CurrentBeatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(baseTestBeatmap.Beatmaps[0]));
AddAssert("selection is updateable beatmap", () => Carousel.CurrentBeatmap, () => Is.EqualTo(BeatmapSets[1].Beatmaps[2]));
AddAssert("visible panel is updateable beatmap", () => (GetSelectedPanel()?.Item?.Model as GroupedBeatmap)?.Beatmap, () => Is.EqualTo(BeatmapSets[1].Beatmaps[2]));
}
/// <summary>
@@ -439,7 +450,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
int originalIndex = BeatmapSets.IndexOf(baseTestBeatmap);
BeatmapSets.ReplaceRange(originalIndex, 1, [updatedSet]);
Realm.Write(r => r.Add(updatedSet, update: true));
BeatmapSets.ReplaceRange(originalIndex, 1, [updatedSet.Detach()]);
});
}
@@ -7,6 +7,11 @@ namespace osu.Game.Online.Matchmaking
{
public interface IMatchmakingServer
{
/// <summary>
/// Retrieves all active matchmaking pools.
/// </summary>
Task<MatchmakingPool[]> GetMatchmakingPools();
/// <summary>
/// Joins the matchmaking lobby, allowing the local user to receive status updates.
/// </summary>
@@ -20,7 +25,7 @@ namespace osu.Game.Online.Matchmaking
/// <summary>
/// Joins the matchmaking queue, allowing the local user to get matched up with others.
/// </summary>
Task MatchmakingJoinQueue(MatchmakingSettings settings);
Task MatchmakingJoinQueue(int poolId);
/// <summary>
/// Leaves the matchmaking queue.
@@ -9,23 +9,31 @@ namespace osu.Game.Online.Matchmaking
{
[MessagePackObject]
[Serializable]
public class MatchmakingSettings : IEquatable<MatchmakingSettings>
public class MatchmakingPool : IEquatable<MatchmakingPool>
{
[Key(0)]
public int RulesetId { get; set; }
public int Id { get; set; }
[Key(1)]
public int RulesetId { get; set; }
[Key(2)]
public int Variant { get; set; }
public bool Equals(MatchmakingSettings? other)
[Key(3)]
public string Name { get; set; } = string.Empty;
public bool Equals(MatchmakingPool? other)
=> other != null
&& Id == other.Id
&& RulesetId == other.RulesetId
&& Variant == other.Variant;
&& Variant == other.Variant
&& Name == other.Name;
public override bool Equals(object? obj)
=> obj is MatchmakingSettings other && Equals(other);
=> obj is MatchmakingPool other && Equals(other);
[SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")]
public override int GetHashCode() => HashCode.Combine(RulesetId, Variant);
public override int GetHashCode() => HashCode.Combine(Id, RulesetId, Variant, Name);
}
}
+44 -20
View File
@@ -19,22 +19,25 @@ namespace osu.Game.Screens.Play.HUD
{
public partial class DefaultRankDisplay : CompositeDrawable, ISerialisableDrawable
{
public bool UsesFixedAnchor { get; set; }
[Resolved]
private ScoreProcessor scoreProcessor { get; set; } = null!;
[SettingSource(typeof(DefaultRankDisplayStrings), nameof(DefaultRankDisplayStrings.PlaySamplesOnRankChange))]
public BindableBool PlaySamples { get; set; } = new BindableBool(true);
public bool UsesFixedAnchor { get; set; }
private UpdateableRank rankDisplay = null!;
private SkinnableSound rankDownSample = null!;
private SkinnableSound rankUpSample = null!;
private Bindable<double?> lastSamplePlaybackTime = null!;
private Bindable<double?> lastSamplePlayback = null!;
private double timeSinceChange;
private IBindable<ScoreRank> rank = null!;
private ScoreRank? displayedRank;
private const int time_before_commit = 1500;
public DefaultRankDisplay()
{
@@ -48,7 +51,7 @@ namespace osu.Game.Screens.Play.HUD
{
rankDownSample = new SkinnableSound(new SampleInfo("Gameplay/rank-down")),
rankUpSample = new SkinnableSound(new SampleInfo("Gameplay/rank-up")),
rankDisplay = new UpdateableRank(ScoreRank.X)
rankDisplay = new UpdateableRank
{
RelativeSizeAxes = Axes.Both
},
@@ -57,31 +60,52 @@ namespace osu.Game.Screens.Play.HUD
if (skinEditor != null)
PlaySamples.Value = false;
lastSamplePlaybackTime = statics.GetBindable<double?>(Static.LastRankChangeSamplePlaybackTime);
lastSamplePlayback = statics.GetBindable<double?>(Static.LastRankChangeSamplePlaybackTime);
}
protected override void LoadComplete()
{
base.LoadComplete();
rank = scoreProcessor.Rank.GetBoundCopy();
rank.BindValueChanged(r =>
updateRank(scoreProcessor.Rank.Value);
}
protected override void Update()
{
base.Update();
var currentRank = scoreProcessor.Rank.Value;
if (currentRank == displayedRank)
{
bool enoughTimeElapsed = !lastSamplePlaybackTime.Value.HasValue || Time.Current - lastSamplePlaybackTime.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME;
timeSinceChange = 0;
return;
}
// Don't play rank-down sfx on quit/retry
if (r.NewValue != r.OldValue && r.NewValue > ScoreRank.F && PlaySamples.Value && enoughTimeElapsed)
{
if (r.NewValue > rankDisplay.Rank)
rankUpSample.Play();
else
rankDownSample.Play();
if ((timeSinceChange += Time.Elapsed) >= time_before_commit || scoreProcessor.HasCompleted.Value || currentRank == ScoreRank.F)
updateRank(currentRank);
}
lastSamplePlaybackTime.Value = Time.Current;
}
private void updateRank(ScoreRank rank)
{
rankDisplay.Rank = rank;
rankDisplay.Rank = r.NewValue;
}, true);
// Check sample time separately to ensure two copies of the rank display don't both play samples on a change.
bool enoughSampleTimeElapsed = !lastSamplePlayback.Value.HasValue || Time.Current - lastSamplePlayback.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME;
// Also don't play rank-down sfx on quit/retry/initial update.
if (displayedRank != null && rank > ScoreRank.F && PlaySamples.Value && enoughSampleTimeElapsed)
{
if (rank > displayedRank)
rankUpSample.Play();
else
rankDownSample.Play();
lastSamplePlayback.Value = Time.Current;
}
displayedRank = rank;
timeSinceChange = 0;
}
}
}
+9 -6
View File
@@ -251,16 +251,19 @@ namespace osu.Game.Screens.SelectV2
newSetBeatmaps.FirstOrDefault(b => b.OnlineID > 0 && b.OnlineID == beatmap.OnlineID) ??
newSetBeatmaps.FirstOrDefault(b => b.DifficultyName == beatmap.DifficultyName && b.Ruleset.Equals(beatmap.Ruleset));
// The matching beatmap may have been deleted or invalidated in some way since this event was fired.
// Let's make sure we have the most up-to-date realm state.
if (matchingNewBeatmap?.ID is Guid matchingID)
matchingNewBeatmap = realm.Run(r => r.FindWithRefresh<BeatmapInfo>(matchingID)?.Detach());
if (matchingNewBeatmap != null)
{
// TODO: should this exist in song select instead of here?
// we need to ensure the global beatmap is also updated alongside changes.
if (CurrentSelection is GroupedBeatmap currentBeatmapUnderGrouping)
{
var candidateSelection = currentBeatmapUnderGrouping with { Beatmap = beatmap };
if (CheckModelEquality(candidateSelection, CurrentSelection))
RequestSelection(candidateSelection);
}
if (CurrentBeatmap != null && beatmap.Equals(CurrentBeatmap))
// we don't know in which group the matching new beatmap is, but that's fine - we can leave it null for now.
// we are about to modify `Items`, which will trigger a re-filter, which will pick a correct group - if one is present - via `HandleFilterCompleted()`.
RequestSelection(new GroupedBeatmap(null, matchingNewBeatmap));
Items.ReplaceRange(previousIndex, 1, [matchingNewBeatmap]);
newSetBeatmaps.Remove(matchingNewBeatmap);
@@ -249,7 +249,7 @@ namespace osu.Game.Screens.SelectV2
mapperText.Text = beatmap.Value.Metadata.Author.Username;
}
starRatingDisplay.Current = (Bindable<StarDifficulty>)difficultyCache.GetBindableDifficulty(beatmap.Value.BeatmapInfo, cancellationSource.Token, SongSelect.SELECTION_DEBOUNCE);
starRatingDisplay.Current = (Bindable<StarDifficulty>)difficultyCache.GetBindableDifficulty(beatmap.Value.BeatmapInfo, cancellationSource.Token, SongSelect.DIFFICULTY_CALCULATION_DEBOUNCE);
updateCountStatistics(cancellationSource.Token);
updateDifficultyStatistics();
+1 -1
View File
@@ -246,7 +246,7 @@ namespace osu.Game.Screens.SelectV2
if (Item == null)
return;
starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token, SongSelect.SELECTION_DEBOUNCE);
starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token, SongSelect.DIFFICULTY_CALCULATION_DEBOUNCE);
starDifficultyBindable.BindValueChanged(starDifficulty =>
{
starRatingDisplay.Current.Value = starDifficulty.NewValue;
@@ -260,7 +260,7 @@ namespace osu.Game.Screens.SelectV2
if (Item == null)
return;
starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token, SongSelect.SELECTION_DEBOUNCE);
starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, starDifficultyCancellationSource.Token, SongSelect.DIFFICULTY_CALCULATION_DEBOUNCE);
starDifficultyBindable.BindValueChanged(starDifficulty =>
{
starRatingDisplay.Current.Value = starDifficulty.NewValue;
+77 -26
View File
@@ -12,6 +12,7 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Development;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@@ -63,9 +64,20 @@ namespace osu.Game.Screens.SelectV2
[Cached(typeof(ISongSelect))]
public abstract partial class SongSelect : ScreenWithBeatmapBackground, IKeyBindingHandler<GlobalAction>, ISongSelect
{
// this is intentionally slightly higher than key repeat, but low enough to not impede user experience.
// this avoids rapid churn loading when iterating the carousel using keyboard.
public const int SELECTION_DEBOUNCE = 100;
/// <summary>
/// A debounce that governs how long after a panel is selected before the rest of song select (and the game at large)
/// updates to show that selection.
///
/// This is intentionally slightly higher than key repeat, but low enough to not impede user experience.
/// </summary>
public const int SELECTION_DEBOUNCE = 150;
/// <summary>
/// A general "global" debounce to be applied to anything aggressive difficulty calculation at song select,
/// either after selection or after a panel comes on screen. Value should be low enough that users don't complain,
/// but otherwise as high as possible to reduce overheads.
/// </summary>
public const int DIFFICULTY_CALCULATION_DEBOUNCE = 150;
private const float logo_scale = 0.4f;
private const double fade_duration = 300;
@@ -371,8 +383,63 @@ namespace osu.Game.Screens.SelectV2
new Dimension(),
new Dimension(GridSizeMode.Relative, 0.5f, minSize: 500, maxSize: 700 + widescreenBonusWidth * 300),
};
if (this.IsCurrentScreen())
updateDebounce();
}
#region Selection debounce
private BeatmapInfo? debounceQueuedSelection;
private double debounceElapsedTime;
private void debounceQueueSelection(BeatmapInfo beatmap)
{
debounceQueuedSelection = beatmap;
debounceElapsedTime = 0;
}
private void updateDebounce()
{
if (debounceQueuedSelection == null) return;
double elapsed = Clock.ElapsedFrameTime;
// avoid debounce running early if there's a single long frame.
if (!DebugUtils.IsNUnitRunning && Clock.FramesPerSecond > 0)
elapsed = Math.Min(1000 / Clock.FramesPerSecond, elapsed);
debounceElapsedTime += elapsed;
if (debounceElapsedTime >= SELECTION_DEBOUNCE)
performDebounceSelection();
}
private void performDebounceSelection()
{
if (debounceQueuedSelection == null) return;
try
{
if (Beatmap.Value.BeatmapInfo.Equals(debounceQueuedSelection))
return;
Beatmap.Value = beatmaps.GetWorkingBeatmap(debounceQueuedSelection);
}
finally
{
cancelDebounceSelection();
}
}
private void cancelDebounceSelection()
{
debounceQueuedSelection = null;
debounceElapsedTime = 0;
}
#endregion
#region Audio
[Resolved]
@@ -436,8 +503,6 @@ namespace osu.Game.Screens.SelectV2
#region Selection handling
private ScheduledDelegate? selectionDebounce;
/// <summary>
/// Finalises selection on the given <see cref="BeatmapInfo"/> and runs the provided action if possible.
/// </summary>
@@ -453,7 +518,7 @@ namespace osu.Game.Screens.SelectV2
// To ensure sanity, cancel any pending selection as we are about to force a selection.
// Carousel selection will update to the forced selection via a call of `ensureGlobalBeatmapValid` below, or when song select becomes current again.
selectionDebounce?.Cancel();
cancelDebounceSelection();
// Forced refetch is important here to guarantee correct invalidation across all difficulties (editor specific).
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap, true);
@@ -482,16 +547,7 @@ namespace osu.Game.Screens.SelectV2
carousel.CurrentGroupedBeatmap = groupedBeatmap;
// Debounce consideration is to avoid beatmap churn on key repeat selection.
selectionDebounce?.Cancel();
selectionDebounce = Scheduler.AddDelayed(() =>
{
var beatmapInfo = groupedBeatmap.Beatmap;
if (Beatmap.Value.BeatmapInfo.Equals(beatmapInfo))
return;
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo);
}, SELECTION_DEBOUNCE);
debounceQueueSelection(groupedBeatmap.Beatmap);
}
private bool ensureGlobalBeatmapValid()
@@ -499,7 +555,7 @@ namespace osu.Game.Screens.SelectV2
if (!this.IsCurrentScreen())
return false;
finaliseBeatmapSelection();
performDebounceSelection();
// While filtering, let's not ever attempt to change selection.
// This will be resolved after the filter completes, see `newItemsPresented`.
@@ -520,7 +576,7 @@ namespace osu.Game.Screens.SelectV2
if (Beatmap.IsDefault)
{
validSelection = carousel.NextRandom();
finaliseBeatmapSelection();
performDebounceSelection();
return validSelection;
}
@@ -542,15 +598,9 @@ namespace osu.Game.Screens.SelectV2
// If all else fails, use the default beatmap.
Beatmap.SetDefault();
finaliseBeatmapSelection();
performDebounceSelection();
return validSelection;
void finaliseBeatmapSelection()
{
if (selectionDebounce?.State == ScheduledDelegate.RunState.Waiting)
selectionDebounce?.RunTask();
}
}
private bool checkBeatmapValidForSelection(BeatmapInfo beatmap, FilterCriteria? criteria)
@@ -797,7 +847,7 @@ namespace osu.Game.Screens.SelectV2
// Interrupting could cause the debounce interval to be reduced.
//
// `ensureGlobalBeatmapValid` is run post-selection which will resolve any pending incompatibilities (see `Beatmap` bindable callback).
if (selectionDebounce?.State != ScheduledDelegate.RunState.Waiting)
if (debounceQueuedSelection == null)
ensureGlobalBeatmapValid();
updateWedgeVisibility();
@@ -1005,6 +1055,7 @@ namespace osu.Game.Screens.SelectV2
return;
onlineLookupCancellation?.Cancel();
onlineLookupCancellation = null;
if (beatmapSetInfo.OnlineID < 0)
{
+58 -35
View File
@@ -34,10 +34,12 @@ namespace osu.Game.Skinning
private SkinnableSound rankDownSample = null!;
private SkinnableSound rankUpSample = null!;
private Bindable<double?> lastSamplePlaybackTime = null!;
private Bindable<double?> lastSamplePlayback = null!;
private double timeSinceChange;
private IBindable<ScoreRank> rank = null!;
private ScoreRank lastRank;
private ScoreRank? displayedRank;
private const int time_before_commit = 1500;
public LegacyRankDisplay()
{
@@ -62,51 +64,72 @@ namespace osu.Game.Skinning
if (skinEditor != null)
PlaySamples.Value = false;
lastSamplePlaybackTime = statics.GetBindable<double?>(Static.LastRankChangeSamplePlaybackTime);
lastSamplePlayback = statics.GetBindable<double?>(Static.LastRankChangeSamplePlaybackTime);
}
protected override void LoadComplete()
{
rank = scoreProcessor.Rank.GetBoundCopy();
rank.BindValueChanged(r =>
base.LoadComplete();
updateRank(scoreProcessor.Rank.Value);
}
protected override void Update()
{
base.Update();
var currentRank = scoreProcessor.Rank.Value;
if (currentRank == displayedRank)
{
var texture = source.GetTexture($"ranking-{r.NewValue}-small");
timeSinceChange = 0;
return;
}
rankDisplay.Texture = texture;
if ((timeSinceChange += Time.Elapsed) >= time_before_commit || scoreProcessor.HasCompleted.Value || currentRank == ScoreRank.F)
updateRank(currentRank);
}
if (texture != null)
private void updateRank(ScoreRank rank)
{
var texture = source.GetTexture($"ranking-{rank}-small");
rankDisplay.Texture = texture;
if (texture != null && displayedRank != null)
{
var transientRank = new Sprite
{
var transientRank = new Sprite
{
Texture = texture,
Blending = BlendingParameters.Additive,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
BypassAutoSizeAxes = Axes.Both,
};
AddInternal(transientRank);
transientRank.FadeOutFromOne(500, Easing.Out)
.ScaleTo(new Vector2(1.625f), 500, Easing.Out)
.Expire();
}
Texture = texture,
Blending = BlendingParameters.Additive,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
BypassAutoSizeAxes = Axes.Both,
};
bool enoughTimeElapsed = !lastSamplePlaybackTime.Value.HasValue || Time.Current - lastSamplePlaybackTime.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME;
AddInternal(transientRank);
// Don't play rank-down sfx on quit/retry
if (r.NewValue != r.OldValue && r.NewValue > ScoreRank.F && PlaySamples.Value && enoughTimeElapsed)
{
if (r.NewValue > lastRank)
rankUpSample.Play();
else
rankDownSample.Play();
transientRank.FadeOutFromOne(500, Easing.Out)
.ScaleTo(new Vector2(1.625f), 500, Easing.Out)
.Expire();
}
lastSamplePlaybackTime.Value = Time.Current;
}
// Check sample time separately to ensure two copies of the rank display don't both play samples on a change.
bool enoughSampleTimeElapsed = !lastSamplePlayback.Value.HasValue || Time.Current - lastSamplePlayback.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME;
lastRank = r.NewValue;
}, true);
// Also don't play rank-down sfx on quit/retry/initial update.
if (displayedRank != null && rank > ScoreRank.F && PlaySamples.Value && enoughSampleTimeElapsed)
{
if (rank > displayedRank)
rankUpSample.Play();
else
rankDownSample.Play();
FinishTransforms(true);
lastSamplePlayback.Value = Time.Current;
}
displayedRank = rank;
timeSinceChange = 0;
}
}
}