From 9b9e7a8f7512c070b256a97bc60fd8318c1beef7 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 12 Sep 2025 18:23:02 +0900 Subject: [PATCH 1/7] Refactor selection roulette SFX logic --- .../Screens/Pick/BeatmapSelectionGrid.cs | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionGrid.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionGrid.cs index 66cae72616..813e8efa0d 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionGrid.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/Screens/Pick/BeatmapSelectionGrid.cs @@ -45,8 +45,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick private bool allowSelection = true; - private readonly Sample[] rouletteSamples = new Sample[8]; - private Sample? rouletteResultSample; + private readonly Sample?[] spinSamples = new Sample?[5]; + private static readonly int[] spin_sample_sequence = [0, 1, 2, 3, 4, 2, 3, 4]; + private Sample? resultSample; private Sample? swooshSample; private double? lastSamplePlayback; @@ -77,15 +78,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick [BackgroundDependencyLoader] private void load(AudioManager audio) { - rouletteSamples[0] = audio.Samples.Get(@"Multiplayer/Matchmaking/roulette-0"); - rouletteSamples[1] = audio.Samples.Get(@"Multiplayer/Matchmaking/roulette-1"); - rouletteSamples[2] = audio.Samples.Get(@"Multiplayer/Matchmaking/roulette-2"); - rouletteSamples[3] = audio.Samples.Get(@"Multiplayer/Matchmaking/roulette-3"); - rouletteSamples[4] = audio.Samples.Get(@"Multiplayer/Matchmaking/roulette-4"); - rouletteSamples[5] = audio.Samples.Get(@"Multiplayer/Matchmaking/roulette-2"); - rouletteSamples[6] = audio.Samples.Get(@"Multiplayer/Matchmaking/roulette-3"); - rouletteSamples[7] = audio.Samples.Get(@"Multiplayer/Matchmaking/roulette-4"); - rouletteResultSample = audio.Samples.Get(@"Multiplayer/Matchmaking/roulette-result"); + for (int i = 0; i < spinSamples.Length; i++) + spinSamples[i] = audio.Samples.Get($@"Multiplayer/Matchmaking/Selection/roulette-{i}"); + + resultSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Selection/roulette-result"); swooshSample = audio.Samples.Get(@"SongSelect/options-pop-out"); } @@ -306,8 +302,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME) { - int sampleIdx = ii % (rouletteSamples.Length); - rouletteSamples[sampleIdx].Play(); + int sequenceIdx = ii % spin_sample_sequence.Length; + spinSamples[spin_sample_sequence[sequenceIdx]]?.Play(); lastSamplePlayback = Time.Current; } @@ -338,7 +334,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Screens.Pick panel.MoveTo(Vector2.Zero, 1000, Easing.OutExpo) .ScaleTo(1.5f, 1000, Easing.OutExpo); - rouletteResultSample?.Play(); + resultSample?.Play(); }); } } From ccc5ca5d806b7a798101efba35a5b2de7ff892f6 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 12 Sep 2025 18:25:08 +0900 Subject: [PATCH 2/7] Rework matchmaking cloud SFX --- osu.Game/Configuration/SessionStatics.cs | 8 ---- .../Matchmaking/MatchmakingCloud.cs | 46 ++++++++++--------- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index 0c0b2a989d..59e107a23e 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -8,7 +8,6 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Scoring; -using osu.Game.Screens.OnlinePlay.Matchmaking; using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; using osu.Game.Users; @@ -29,7 +28,6 @@ namespace osu.Game.Configuration SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null); SetDefault(Static.LastModSelectPanelSamplePlaybackTime, (double?)null); SetDefault(Static.LastRankChangeSamplePlaybackTime, (double?)null); - SetDefault(Static.LastMatchmakingCloudSamplePlaybackTime, (double?)null); SetDefault(Static.SeasonalBackgrounds, null); SetDefault(Static.TouchInputActive, RuntimeInfo.IsMobile); SetDefault(Static.LastLocalUserScore, null); @@ -83,12 +81,6 @@ namespace osu.Game.Configuration /// LastRankChangeSamplePlaybackTime, - /// - /// The last playback time in milliseconds for the 'user appear' sample in . - /// Used to debounce sample playback to avoid volume saturation from multiple simultaneous playback. - /// - LastMatchmakingCloudSamplePlaybackTime, - /// /// Whether the last positional input received was a touch input. /// Used in touchscreen detection scenarios (). diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingCloud.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingCloud.cs index d2b2b72f02..3fab5ab207 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingCloud.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingCloud.cs @@ -10,7 +10,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; -using osu.Game.Configuration; using osu.Game.Online.API.Requests.Responses; using osu.Game.Screens.Ranking; using osuTK; @@ -22,6 +21,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking private APIUser[] users = []; private Container usersContainer = null!; + private readonly Bindable lastSamplePlayback = new Bindable(); + public APIUser[] Users { get => users; @@ -32,7 +33,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking foreach (var u in usersContainer) u.Delay(RNG.Next(0, 1000)).FadeOut(500).Expire(); - LoadComponentsAsync(users.Select(u => new MovingAvatar(u)), avatars => + LoadComponentsAsync(users.Select(u => new MovingAvatar(u, lastSamplePlayback)), avatars => { if (usersContainer.Count == 0) { @@ -69,24 +70,25 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking private float targetScale; private float targetAlpha; - private Bindable lastSamplePlaybackTime = null!; + private readonly Bindable lastSamplePlayback = new Bindable(); + private const int num_appear_samples = 6; private Sample? playerAppearSample; - public MovingAvatar(APIUser apiUser) + public MovingAvatar(APIUser apiUser, Bindable lastSamplePlayback) : base(apiUser) { RelativePositionAxes = Axes.Both; Scale = new Vector2(2); Origin = Anchor.Centre; + this.lastSamplePlayback.BindTo(lastSamplePlayback); } [BackgroundDependencyLoader] - private void load(AudioManager audio, SessionStatics statics) + private void load(AudioManager audio) { - playerAppearSample = audio.Samples.Get(@"UI/toolbar-hover"); - lastSamplePlaybackTime = statics.GetBindable(Static.LastMatchmakingCloudSamplePlaybackTime); + playerAppearSample = audio.Samples.Get($@"Multiplayer/Matchmaking/Cloud/appear-{RNG.Next(0, num_appear_samples)}"); } protected override void LoadComplete() @@ -103,20 +105,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking Hide(); int appearDelay = RNG.Next(0, 1000); this.Delay(appearDelay).FadeTo(targetAlpha, 2000, Easing.OutQuint); - Scheduler.AddDelayed(() => - { - bool enoughTimeElapsed = !lastSamplePlaybackTime.Value.HasValue || Time.Current - lastSamplePlaybackTime.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME; - if (!enoughTimeElapsed) return; - - var chan = playerAppearSample?.GetChannel(); - - if (chan == null) return; - - chan.Frequency.Value = 1f + RNG.NextDouble(0.25f); - chan.Play(); - - lastSamplePlaybackTime.Value = Time.Current; - }, appearDelay); + Scheduler.AddDelayed(playAppearSample, appearDelay); } private void updateParams() @@ -128,6 +117,21 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking Scheduler.AddDelayed(updateParams, RNG.Next(500, 5000)); } + private void playAppearSample() + { + bool enoughTimeElapsed = !lastSamplePlayback.Value.HasValue || Time.Current - lastSamplePlayback.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME; + if (!enoughTimeElapsed) return; + + var chan = playerAppearSample?.GetChannel(); + if (chan == null) return; + + chan.Frequency.Value = 0.5f + RNG.NextDouble(1.5f); + chan.Balance.Value = MathF.Cos(angle) * OsuGameBase.SFX_STEREO_STRENGTH; + chan.Play(); + + lastSamplePlayback.Value = Time.Current; + } + protected override void Update() { base.Update(); From 9a2513230cc3661898b6aa8212856ede5741f115 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 12 Sep 2025 18:27:16 +0900 Subject: [PATCH 3/7] Add SFX for stage progression feedback --- .../OnlinePlay/Matchmaking/StageBubble.cs | 16 +++++++++++++++- .../Screens/OnlinePlay/Matchmaking/StageText.cs | 15 ++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/StageBubble.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/StageBubble.cs index 281374ba71..2ebd3376d3 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/StageBubble.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/StageBubble.cs @@ -3,6 +3,8 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; @@ -31,6 +33,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking private DateTimeOffset countdownStartTime; private DateTimeOffset countdownEndTime; + private Sample? stageProgressSample; + private double? lastSamplePlayback; + public StageBubble(MatchmakingStage stage, LocalisableString displayText) { this.stage = stage; @@ -40,7 +45,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking } [BackgroundDependencyLoader] - private void load() + private void load(AudioManager audio) { InternalChild = new CircularContainer { @@ -68,6 +73,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking } } }; + + stageProgressSample = audio.Samples.Get(@"Multiplayer/countdown-tick"); } protected override void LoadComplete() @@ -98,6 +105,13 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking { TimeSpan elapsed = DateTimeOffset.Now - countdownStartTime; progressBar.Width = (float)(elapsed.TotalMilliseconds / duration.TotalMilliseconds); + + bool enoughTimeElapsed = lastSamplePlayback == null || Time.Current - lastSamplePlayback >= 1000f; + if (elapsed.TotalMilliseconds < 1000f || !enoughTimeElapsed || elapsed.TotalMilliseconds >= duration.TotalMilliseconds) + return; + + stageProgressSample?.Play(); + lastSamplePlayback = Time.Current; } } diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/StageText.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/StageText.cs index ab2627474e..b47e135004 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/StageText.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/StageText.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -20,13 +22,16 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking private OsuSpriteText text = null!; + private Sample? textChangedSample; + private double? lastSamplePlayback; + public StageText() { AutoSizeAxes = Axes.Both; } [BackgroundDependencyLoader] - private void load() + private void load(AudioManager audio) { InternalChild = text = new OsuSpriteText { @@ -34,6 +39,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking Font = OsuFont.Default, AlwaysPresent = true, }; + + textChangedSample = audio.Samples.Get(@"Multiplayer/Matchmaking/stage-message"); } protected override void LoadComplete() @@ -50,6 +57,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking return; text.Text = getTextForStatus(matchmakingState.Stage); + + if (text.Text == string.Empty || (lastSamplePlayback != null && Time.Current - lastSamplePlayback < OsuGameBase.SAMPLE_DEBOUNCE_TIME)) + return; + + textChangedSample?.Play(); + lastSamplePlayback = Time.Current; }); private LocalisableString getTextForStatus(MatchmakingStage status) From a9c021ce04c45ee2c7d0172d2ae6a8aedcc7be87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 13 Sep 2025 10:50:33 +0900 Subject: [PATCH 4/7] Demonstrate failure in test --- .../Visual/Gameplay/TestSceneArgonJudgementCounter.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonJudgementCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonJudgementCounter.cs index e08af79032..e5886aa607 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneArgonJudgementCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneArgonJudgementCounter.cs @@ -183,6 +183,9 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Show all judgements", () => counterDisplay.Mode.Value = ArgonJudgementCounterDisplay.DisplayMode.All); AddWaitStep("wait some", 2); AddAssert("Check all visible", () => counterDisplay.CounterFlow.ChildrenOfType().Last().Alpha == 1); + AddToggleStep("toggle wireframe display", t => counterDisplay.WireframeOpacity.Value = t ? 0.3f : 0); + AddStep("Set direction vertical", () => counterDisplay.FlowDirection.Value = Direction.Vertical); + AddStep("Set direction horizontal", () => counterDisplay.FlowDirection.Value = Direction.Horizontal); } private int hiddenCount() From e73e9275baeeaa024f3e3401309b7e56a5029533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 13 Sep 2025 11:11:45 +0900 Subject: [PATCH 5/7] Fix argon judgement counter looking misaligned with wireframe off Closes https://github.com/ppy/osu/issues/34959. `ArgonCounterTextComponent` is pretty terrible and prevents being able to fix the issue easily. The core issue is that this is the first instance of the component's usage where the label text can be longer than the counter in the X dimension, so the total width of any counter is equal to max(label width, counter width), and the label will be aligned to the left of that width, while the counter will be aligned to the right of that width. The fix sort of relies on the fact that I don't expect *any* consumer of `ArgonCounterTextComponent` that meaningfully uses the wireframe digits to want the non-wireframe digits to be aligned to the *left* rather than the right. It's not what I'd expect any segmented display to work. (There are usages that specify `TopLeft` anchor, but they usually display the same number of wireframe and non-wireframe digits, so for them it doesn't really matter if the digits are left-aligned to the wireframes or not.) --- osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs | 8 ++++---- osu.Game/Skinning/Components/ArgonJudgementCounter.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs index 3789fb1645..d55bf46f97 100644 --- a/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs +++ b/osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs @@ -74,13 +74,13 @@ namespace osu.Game.Screens.Play.HUD { wireframesPart = new ArgonCounterSpriteText(wireframesLookup) { - Anchor = anchor, - Origin = anchor, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, }, textPart = new ArgonCounterSpriteText(textLookup) { - Anchor = anchor, - Origin = anchor, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, }, } } diff --git a/osu.Game/Skinning/Components/ArgonJudgementCounter.cs b/osu.Game/Skinning/Components/ArgonJudgementCounter.cs index 6fe8ac7ecd..84973aab3e 100644 --- a/osu.Game/Skinning/Components/ArgonJudgementCounter.cs +++ b/osu.Game/Skinning/Components/ArgonJudgementCounter.cs @@ -37,7 +37,7 @@ namespace osu.Game.Skinning.Components Result = result; AutoSizeAxes = Axes.Both; - AddInternal(textComponent = new ArgonCounterTextComponent(Anchor.TopRight, result.DisplayName.ToUpper())); + AddInternal(textComponent = new ArgonCounterTextComponent(Anchor.TopLeft, result.DisplayName.ToUpper())); } private void updateWireframe() From 4ccfebe8424c4cd7912b9b08f274ee79284cef56 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 14 Sep 2025 14:15:16 +0900 Subject: [PATCH 6/7] Update resources --- osu.Game/osu.Game.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 122a927abe..d64fadee97 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ - + From 9577472c9e3c9444fb6682aaf3266832f64cb707 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 15 Sep 2025 12:49:42 +0900 Subject: [PATCH 7/7] Fix errors in gameplay stage of matchmaking --- .../OnlinePlay/Matchmaking/MatchmakingScreen.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs index ba2e5593bf..b02583103d 100644 --- a/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Matchmaking/MatchmakingScreen.cs @@ -217,6 +217,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking private void updateGameplayState() { MultiplayerPlaylistItem item = client.Room!.CurrentPlaylistItem; + + if (item.Expired) + return; + RulesetInfo ruleset = rulesets.GetRuleset(item.RulesetID)!; Ruleset rulesetInstance = ruleset.CreateInstance(); @@ -228,9 +232,15 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking Mods.Value = item.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); if (Beatmap.Value is DummyWorkingBeatmap) - client.ChangeState(MultiplayerUserState.Idle).FireAndForget(); + { + if (client.LocalUser!.State == MultiplayerUserState.Ready) + client.ChangeState(MultiplayerUserState.Idle).FireAndForget(); + } else - client.ChangeState(MultiplayerUserState.Ready).FireAndForget(); + { + if (client.LocalUser!.State == MultiplayerUserState.Idle) + client.ChangeState(MultiplayerUserState.Ready).FireAndForget(); + } client.ChangeBeatmapAvailability(beatmapAvailabilityTracker.Availability.Value).FireAndForget(); }