From a6f823e5bc32bfc0e59fa4a0df82e1c2fff263b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 21 Aug 2025 14:18:13 +0200 Subject: [PATCH 01/38] Show pinned rooms on top of listing --- .../Visual/Multiplayer/TestSceneRoomPanel.cs | 15 ++++++++++++--- osu.Game/Online/Rooms/Room.cs | 9 +++++++++ .../OnlinePlay/Lounge/Components/RoomListing.cs | 3 +-- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs index 037c5faae3..ce9ee3a011 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs @@ -81,6 +81,15 @@ namespace osu.Game.Tests.Visual.Multiplayer CurrentPlaylistItem = item1 }), createLoungeRoom(new Room + { + Name = "Pinned room", + Pinned = true, + EndDate = DateTimeOffset.Now.AddDays(1), + Type = MatchType.HeadToHead, + Playlist = [item1], + CurrentPlaylistItem = item1 + }), + createLoungeRoom(new Room { Name = "Private room", Password = "*", @@ -140,13 +149,13 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for panel load", () => panel.ChildrenOfType().Any()); - AddAssert("password icon hidden", () => Precision.AlmostEquals(0, panel.ChildrenOfType().Single().Alpha)); + AddAssert("password icon hidden", () => Precision.AlmostEquals(0, panel.ChildrenOfType().Single().Alpha)); AddStep("set password", () => room.Password = "password"); - AddAssert("password icon visible", () => Precision.AlmostEquals(1, panel.ChildrenOfType().Single().Alpha)); + AddAssert("password icon visible", () => Precision.AlmostEquals(1, panel.ChildrenOfType().Single().Alpha)); AddStep("unset password", () => room.Password = string.Empty); - AddAssert("password icon hidden", () => Precision.AlmostEquals(0, panel.ChildrenOfType().Single().Alpha)); + AddAssert("password icon hidden", () => Precision.AlmostEquals(0, panel.ChildrenOfType().Single().Alpha)); } [Test] diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index e965f9c187..4200fed0dd 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -263,6 +263,12 @@ namespace osu.Game.Online.Rooms set => SetField(ref availability, value); } + public bool Pinned + { + get => pinned; + set => SetField(ref pinned, value); + } + [JsonProperty("id")] private long? roomId; @@ -339,6 +345,9 @@ namespace osu.Game.Online.Rooms [JsonConverter(typeof(SnakeCaseStringEnumConverter))] private RoomStatus status; + [JsonProperty("pinned")] + private bool pinned; + // Not yet serialised (not implemented). private RoomAvailability availability; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs index 14edd13ec5..b93d26880d 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs @@ -194,8 +194,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components roomFlow.Add(drawableRoom); - // Always show spotlight playlists at the top of the listing. - roomFlow.SetLayoutPosition(drawableRoom, room.Category > RoomCategory.Normal ? float.MinValue : -(room.RoomID ?? 0)); + roomFlow.SetLayoutPosition(drawableRoom, room.Pinned ? float.MinValue : -(room.RoomID ?? 0)); } applyFilterCriteria(Filter.Value); From 20b316d32d3cd4a575e31df5608209a0491588da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 21 Aug 2025 14:18:47 +0200 Subject: [PATCH 02/38] Add indicator for pinned rooms in upper right of room panel --- .../Visual/Multiplayer/TestSceneRoomPanel.cs | 6 +- .../OnlinePlay/Lounge/Components/RoomPanel.cs | 62 ++++++++++++++----- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs index ce9ee3a011..d1b9005e63 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs @@ -149,13 +149,13 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for panel load", () => panel.ChildrenOfType().Any()); - AddAssert("password icon hidden", () => Precision.AlmostEquals(0, panel.ChildrenOfType().Single().Alpha)); + AddAssert("password icon hidden", () => Precision.AlmostEquals(0, panel.ChildrenOfType().First().Alpha)); AddStep("set password", () => room.Password = "password"); - AddAssert("password icon visible", () => Precision.AlmostEquals(1, panel.ChildrenOfType().Single().Alpha)); + AddAssert("password icon visible", () => Precision.AlmostEquals(1, panel.ChildrenOfType().First().Alpha)); AddStep("unset password", () => room.Password = string.Empty); - AddAssert("password icon hidden", () => Precision.AlmostEquals(0, panel.ChildrenOfType().Single().Alpha)); + AddAssert("password icon hidden", () => Precision.AlmostEquals(0, panel.ChildrenOfType().First().Alpha)); } [Test] diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs index 3610995b2c..258c9c3a97 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs @@ -59,7 +59,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private DrawableRoomParticipantsList? drawableRoomParticipantsList; private RoomSpecialCategoryPill? specialCategoryPill; - private PasswordProtectedIcon? passwordIcon; + private CornerIcon? passwordIcon; + private CornerIcon? pinnedIcon; private EndDateInfo? endDateInfo; private RoomNameLine? roomName; private DelayedLoadWrapper wrapper = null!; @@ -88,7 +89,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } [BackgroundDependencyLoader] - private void load(OverlayColourProvider colours) + private void load(OverlayColourProvider colourProvider, OsuColour colours) { ButtonsContainer = new Container { @@ -104,7 +105,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components new Box { RelativeSizeAxes = Axes.Both, - Colour = colours.Background5, + Colour = colourProvider.Background5, }, CreateBackground().With(d => { @@ -128,7 +129,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components new Box { RelativeSizeAxes = Axes.Both, - Colour = colours.Background5, + Colour = colourProvider.Background5, Width = 0.2f, }, new Box @@ -136,7 +137,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Anchor = Anchor.TopRight, Origin = Anchor.TopRight, RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(colours.Background5, colours.Background5.Opacity(0.3f)), + Colour = ColourInfo.GradientHorizontal(colourProvider.Background5, colourProvider.Background5.Opacity(0.3f)), Width = 0.8f, }, new GridContainer @@ -254,7 +255,28 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } }, - passwordIcon = new PasswordProtectedIcon { Alpha = 0 } + passwordIcon = new CornerIcon + { + Alpha = 0, + Background = { Colour = colours.Gray8, }, + Icon = + { + Icon = FontAwesome.Solid.Lock, + Colour = colours.Gray3, + Rotation = 45, + }, + }, + pinnedIcon = new CornerIcon + { + Alpha = 0, + Background = { Colour = colours.Orange2 }, + Icon = + { + Icon = FontAwesome.Solid.Thumbtack, + Colour = colours.Gray3, + Rotation = 45, + }, + } }, }, }, 0) @@ -283,6 +305,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components updateRoomCategory(); updateRoomType(); updateRoomHasPassword(); + updateRoomPinned(); }; SelectedItem.BindValueChanged(onSelectedItemChanged, true); @@ -311,6 +334,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components case nameof(Room.HasPassword): updateRoomHasPassword(); break; + + case nameof(Room.Pinned): + updateRoomPinned(); + break; } } @@ -371,6 +398,12 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components passwordIcon.Alpha = Room.HasPassword ? 1 : 0; } + private void updateRoomPinned() + { + if (pinnedIcon != null) + pinnedIcon.Alpha = Room.Pinned ? 1 : 0; + } + private int numberOfAvatars = 7; public int NumberOfAvatars @@ -534,10 +567,12 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } - public partial class PasswordProtectedIcon : CompositeDrawable + public partial class CornerIcon : CompositeDrawable { - [BackgroundDependencyLoader] - private void load(OsuColour colours) + public SpriteIcon Icon { get; } + public Box Background { get; } + + public CornerIcon() { Anchor = Anchor.TopRight; Origin = Anchor.TopRight; @@ -546,20 +581,19 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components InternalChildren = new Drawable[] { - new Box + Background = new Box { Anchor = Anchor.TopRight, Origin = Anchor.TopCentre, - Colour = colours.Gray5, Rotation = 45, RelativeSizeAxes = Axes.Both, Width = 2, }, - new SpriteIcon + Icon = new SpriteIcon { - Icon = FontAwesome.Solid.Lock, Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, + Origin = Anchor.Centre, + Position = new Vector2(-13, 13), Margin = new MarginPadding(6), Size = new Vector2(14), } From 03e7e2b0d856f3164a872fc9d72bb298418c6535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 22 Aug 2025 11:26:48 +0200 Subject: [PATCH 03/38] Update tests --- .../Visual/Multiplayer/TestSceneRoomListing.cs | 14 +++++++------- .../Visual/Multiplayer/TestSceneRoomPanel.cs | 6 +++--- .../Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs | 6 +++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs index 58473f5fa2..7f6fb97e0c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomListing.cs @@ -52,25 +52,25 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestBasicListChanges() { - AddStep("add rooms", () => rooms.AddRange(GenerateRooms(5, withSpotlightRooms: true))); + AddStep("add rooms", () => rooms.AddRange(GenerateRooms(5, withPinnedRooms: true))); AddAssert("has 5 rooms", () => container.DrawableRooms.Count == 5); - AddAssert("all spotlights at top", () => container.DrawableRooms - .SkipWhile(r => r.Room.Category == RoomCategory.Spotlight) - .All(r => r.Room.Category == RoomCategory.Normal)); + AddAssert("all pinned at top", () => container.DrawableRooms + .SkipWhile(r => r.Room.Pinned) + .All(r => !r.Room.Pinned)); AddStep("remove first room", () => rooms.RemoveAt(0)); AddAssert("has 4 rooms", () => container.DrawableRooms.Count == 4); AddAssert("first room removed", () => container.DrawableRooms.All(r => r.Room.RoomID != 0)); AddStep("select first room", () => container.DrawableRooms.First().TriggerClick()); - AddAssert("first spotlight selected", () => checkRoomSelected(rooms.First(r => r.Category == RoomCategory.Spotlight))); + AddAssert("first pinned room selected", () => checkRoomSelected(rooms.First(r => r.Pinned))); AddStep("remove last room", () => rooms.RemoveAt(rooms.Count - 1)); - AddAssert("first spotlight still selected", () => checkRoomSelected(rooms.First(r => r.Category == RoomCategory.Spotlight))); + AddAssert("first pinned room selected", () => checkRoomSelected(rooms.First(r => r.Pinned))); - AddStep("remove spotlight room", () => rooms.RemoveAll(r => r.Category == RoomCategory.Spotlight)); + AddStep("remove pinned rooms", () => rooms.RemoveAll(r => r.Pinned)); AddAssert("selection vacated", () => checkRoomSelected(null)); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs index d1b9005e63..58eb0f1ea1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs @@ -130,9 +130,9 @@ namespace osu.Game.Tests.Visual.Multiplayer }; }); - AddUntilStep("wait for panel load", () => rooms.Count == 7); - AddUntilStep("correct status text", () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Currently playing", StringComparison.Ordinal)) == 2); - AddUntilStep("correct status text", () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)) == 5); + AddUntilStep("wait for panel load", () => rooms.Count, () => Is.EqualTo(8)); + AddUntilStep("\"currently playing\" room count correct", () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Currently playing", StringComparison.Ordinal)), () => Is.EqualTo(3)); + AddUntilStep("\"ready to play\" room count correct", () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)), () => Is.EqualTo(4)); } [Test] diff --git a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs index 914d187864..c687815270 100644 --- a/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs +++ b/osu.Game/Tests/Visual/OnlinePlay/OnlinePlayTestScene.cs @@ -95,7 +95,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay /// protected virtual OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new OnlinePlayTestSceneDependencies(); - protected Room[] GenerateRooms(int count, RulesetInfo? ruleset = null, bool withPassword = false, bool withSpotlightRooms = false) + protected Room[] GenerateRooms(int count, RulesetInfo? ruleset = null, bool withPassword = false, bool withPinnedRooms = false) { Room[] rooms = new Room[count]; @@ -110,10 +110,10 @@ namespace osu.Game.Tests.Visual.OnlinePlay Name = $@"Room {currentRoomId}", Host = new APIUser { Username = @"Host" }, Duration = TimeSpan.FromSeconds(10), - Category = withSpotlightRooms && i % 2 == 0 ? RoomCategory.Spotlight : RoomCategory.Normal, Password = withPassword ? @"password" : null, PlaylistItemStats = new Room.RoomPlaylistItemStats { RulesetIDs = [ruleset.OnlineID] }, - Playlist = [new PlaylistItem(new BeatmapInfo { Metadata = new BeatmapMetadata() }) { RulesetID = ruleset.OnlineID }] + Playlist = [new PlaylistItem(new BeatmapInfo { Metadata = new BeatmapMetadata() }) { RulesetID = ruleset.OnlineID }], + Pinned = withPinnedRooms && i % 2 == 0, }; } From b95573f97df469dafe331631fc73b04f98844d80 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 28 Aug 2025 13:20:02 +0900 Subject: [PATCH 04/38] Fix potential loss of room events during join --- .../Online/Multiplayer/MultiplayerClient.cs | 181 ++++++++++-------- 1 file changed, 99 insertions(+), 82 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 986bc26716..14bc0bad82 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -172,6 +172,8 @@ namespace osu.Game.Online.Multiplayer protected Room? APIRoom { get; private set; } + private readonly Queue pendingRequests = new Queue(); + [BackgroundDependencyLoader] private void load() { @@ -266,6 +268,9 @@ namespace osu.Game.Online.Multiplayer updateLocalRoomSettings(joinedRoom.Settings); + while (pendingRequests.TryDequeue(out Action? action)) + action(); + postServerShuttingDownNotification(); OnRoomJoined(); @@ -300,10 +305,23 @@ namespace osu.Game.Online.Multiplayer RoomUpdated?.Invoke(); }); - return joinOrLeaveTaskChain.Add(async () => + return Task.Run(async () => { - await scheduledReset.ConfigureAwait(false); - await LeaveRoomInternal().ConfigureAwait(false); + try + { + await joinOrLeaveTaskChain.Add(async () => + { + await scheduledReset.ConfigureAwait(false); + await LeaveRoomInternal().ConfigureAwait(false); + }); + } + finally + { + await runOnUpdateThreadAsync(() => + { + pendingRequests.Clear(); + }); + } }); } @@ -449,11 +467,9 @@ namespace osu.Game.Online.Multiplayer Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state) { - Scheduler.Add(() => + handleRoomRequest(() => { - if (Room == null) - return; - + Debug.Assert(Room != null); Debug.Assert(APIRoom != null); Room.State = state; @@ -476,7 +492,7 @@ namespace osu.Game.Online.Multiplayer } RoomUpdated?.Invoke(); - }, false); + }); return Task.CompletedTask; } @@ -485,10 +501,9 @@ namespace osu.Game.Online.Multiplayer { await PopulateUsers([user]).ConfigureAwait(false); - Scheduler.Add(() => + handleRoomRequest(() => { - if (Room == null) - return; + Debug.Assert(Room != null); // for sanity, ensure that there can be no duplicate users in the room user list. if (Room.Users.Any(existing => existing.UserID == user.UserID)) @@ -500,18 +515,18 @@ namespace osu.Game.Online.Multiplayer UserJoined?.Invoke(user); RoomUpdated?.Invoke(); - }, false); + }); } Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user) { - Scheduler.Add(() => handleUserLeft(user, UserLeft), false); + handleRoomRequest(() => handleUserLeft(user, UserLeft)); return Task.CompletedTask; } Task IMultiplayerClient.UserKicked(MultiplayerRoomUser user) { - Scheduler.Add(() => + handleRoomRequest(() => { if (LocalUser == null) return; @@ -520,7 +535,7 @@ namespace osu.Game.Online.Multiplayer LeaveRoom(); handleUserLeft(user, UserKicked); - }, false); + }); return Task.CompletedTask; } @@ -528,9 +543,7 @@ namespace osu.Game.Online.Multiplayer private void handleUserLeft(MultiplayerRoomUser user, Action? callback) { Debug.Assert(ThreadSafety.IsUpdateThread); - - if (Room == null) - return; + Debug.Assert(Room != null); Room.Users.Remove(user); PlayingUserIds.Remove(user.UserID); @@ -587,11 +600,9 @@ namespace osu.Game.Online.Multiplayer Task IMultiplayerClient.HostChanged(int userId) { - Scheduler.Add(() => + handleRoomRequest(() => { - if (Room == null) - return; - + Debug.Assert(Room != null); Debug.Assert(APIRoom != null); var user = Room.Users.FirstOrDefault(u => u.UserID == userId); @@ -601,22 +612,24 @@ namespace osu.Game.Online.Multiplayer HostChanged?.Invoke(user); RoomUpdated?.Invoke(); - }, false); + }); return Task.CompletedTask; } Task IMultiplayerClient.SettingsChanged(MultiplayerRoomSettings newSettings) { - Scheduler.Add(() => updateLocalRoomSettings(newSettings)); + handleRoomRequest(() => updateLocalRoomSettings(newSettings)); return Task.CompletedTask; } Task IMultiplayerClient.UserStateChanged(int userId, MultiplayerUserState state) { - Scheduler.Add(() => + handleRoomRequest(() => { - var user = Room?.Users.SingleOrDefault(u => u.UserID == userId); + Debug.Assert(Room != null); + + var user = Room.Users.SingleOrDefault(u => u.UserID == userId); // TODO: user should NEVER be null here, see https://github.com/ppy/osu/issues/17713. if (user == null) @@ -626,16 +639,18 @@ namespace osu.Game.Online.Multiplayer updateUserPlayingState(userId, state); RoomUpdated?.Invoke(); - }, false); + }); return Task.CompletedTask; } Task IMultiplayerClient.MatchUserStateChanged(int userId, MatchUserState state) { - Scheduler.Add(() => + handleRoomRequest(() => { - var user = Room?.Users.SingleOrDefault(u => u.UserID == userId); + Debug.Assert(Room != null); + + var user = Room.Users.SingleOrDefault(u => u.UserID == userId); // TODO: user should NEVER be null here, see https://github.com/ppy/osu/issues/17713. if (user == null) @@ -643,31 +658,29 @@ namespace osu.Game.Online.Multiplayer user.MatchState = state; RoomUpdated?.Invoke(); - }, false); + }); return Task.CompletedTask; } Task IMultiplayerClient.MatchRoomStateChanged(MatchRoomState state) { - Scheduler.Add(() => + handleRoomRequest(() => { - if (Room == null) - return; + Debug.Assert(Room != null); Room.MatchState = state; RoomUpdated?.Invoke(); - }, false); + }); return Task.CompletedTask; } public Task MatchEvent(MatchServerEvent e) { - Scheduler.Add(() => + handleRoomRequest(() => { - if (Room == null) - return; + Debug.Assert(Room != null); switch (e) { @@ -691,7 +704,7 @@ namespace osu.Game.Online.Multiplayer } RoomUpdated?.Invoke(); - }, false); + }); return Task.CompletedTask; } @@ -708,9 +721,11 @@ namespace osu.Game.Online.Multiplayer Task IMultiplayerClient.UserBeatmapAvailabilityChanged(int userId, BeatmapAvailability beatmapAvailability) { - Scheduler.Add(() => + handleRoomRequest(() => { - var user = Room?.Users.SingleOrDefault(u => u.UserID == userId); + Debug.Assert(Room != null); + + var user = Room.Users.SingleOrDefault(u => u.UserID == userId); // errors here are not critical - beatmap availability state is mostly for display. if (user == null) @@ -719,16 +734,18 @@ namespace osu.Game.Online.Multiplayer user.BeatmapAvailability = beatmapAvailability; RoomUpdated?.Invoke(); - }, false); + }); return Task.CompletedTask; } Task IMultiplayerClient.UserStyleChanged(int userId, int? beatmapId, int? rulesetId) { - Scheduler.Add(() => + handleRoomRequest(() => { - var user = Room?.Users.SingleOrDefault(u => u.UserID == userId); + Debug.Assert(Room != null); + + var user = Room.Users.SingleOrDefault(u => u.UserID == userId); // errors here are not critical - user style is mostly for display. if (user == null) @@ -739,16 +756,18 @@ namespace osu.Game.Online.Multiplayer UserStyleChanged?.Invoke(user); RoomUpdated?.Invoke(); - }, false); + }); return Task.CompletedTask; } Task IMultiplayerClient.UserModsChanged(int userId, IEnumerable mods) { - Scheduler.Add(() => + handleRoomRequest(() => { - var user = Room?.Users.SingleOrDefault(u => u.UserID == userId); + Debug.Assert(Room != null); + + var user = Room.Users.SingleOrDefault(u => u.UserID == userId); // errors here are not critical - user mods are mostly for display. if (user == null) @@ -758,70 +777,60 @@ namespace osu.Game.Online.Multiplayer UserModsChanged?.Invoke(user); RoomUpdated?.Invoke(); - }, false); + }); return Task.CompletedTask; } Task IMultiplayerClient.LoadRequested() { - Scheduler.Add(() => + handleRoomRequest(() => { - if (Room == null) - return; - + Debug.Assert(Room != null); LoadRequested?.Invoke(); - }, false); + }); return Task.CompletedTask; } Task IMultiplayerClient.GameplayAborted(GameplayAbortReason reason) { - Scheduler.Add(() => + handleRoomRequest(() => { - if (Room == null) - return; - + Debug.Assert(Room != null); GameplayAborted?.Invoke(reason); - }, false); + }); return Task.CompletedTask; } Task IMultiplayerClient.GameplayStarted() { - Scheduler.Add(() => + handleRoomRequest(() => { - if (Room == null) - return; - + Debug.Assert(Room != null); GameplayStarted?.Invoke(); - }, false); + }); return Task.CompletedTask; } Task IMultiplayerClient.ResultsReady() { - Scheduler.Add(() => + handleRoomRequest(() => { - if (Room == null) - return; - + Debug.Assert(Room != null); ResultsReady?.Invoke(); - }, false); + }); return Task.CompletedTask; } public Task PlaylistItemAdded(MultiplayerPlaylistItem item) { - Scheduler.Add(() => + handleRoomRequest(() => { - if (Room == null) - return; - + Debug.Assert(Room != null); Debug.Assert(APIRoom != null); Room.Playlist.Add(item); @@ -836,11 +845,9 @@ namespace osu.Game.Online.Multiplayer public Task PlaylistItemRemoved(long playlistItemId) { - Scheduler.Add(() => + handleRoomRequest(() => { - if (Room == null) - return; - + Debug.Assert(Room != null); Debug.Assert(APIRoom != null); Room.Playlist.Remove(Room.Playlist.Single(existing => existing.ID == playlistItemId)); @@ -857,11 +864,9 @@ namespace osu.Game.Online.Multiplayer public Task PlaylistItemChanged(MultiplayerPlaylistItem item) { - Scheduler.Add(() => + handleRoomRequest(() => { - if (Room == null) - return; - + Debug.Assert(Room != null); Debug.Assert(APIRoom != null); Room.Playlist[Room.Playlist.IndexOf(Room.Playlist.Single(existing => existing.ID == item.ID))] = item; @@ -908,9 +913,7 @@ namespace osu.Game.Online.Multiplayer /// The new to update from. private void updateLocalRoomSettings(MultiplayerRoomSettings settings) { - if (Room == null) - return; - + Debug.Assert(Room != null); Debug.Assert(APIRoom != null); // Update a few properties of the room instantaneously. @@ -972,6 +975,20 @@ namespace osu.Game.Online.Multiplayer return tcs.Task; } + private void handleRoomRequest(Action request) + { + Scheduler.Add(() => + { + if (Room == null) + { + pendingRequests.Enqueue(request); + return; + } + + request(); + }); + } + Task IStatefulUserHubClient.DisconnectRequested() { Schedule(() => From 9ae6e509b73ca265d1fac781a428b2e9a564fe2d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 28 Aug 2025 13:47:55 +0900 Subject: [PATCH 05/38] Configure await calls --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 14bc0bad82..1dfa3c0cfb 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -313,14 +313,14 @@ namespace osu.Game.Online.Multiplayer { await scheduledReset.ConfigureAwait(false); await LeaveRoomInternal().ConfigureAwait(false); - }); + }).ConfigureAwait(false); } finally { await runOnUpdateThreadAsync(() => { pendingRequests.Clear(); - }); + }).ConfigureAwait(false); } }); } From df6d6edaca6e69736ddd52ff78cf026717aae935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 29 Aug 2025 09:29:47 +0200 Subject: [PATCH 06/38] Fix song select not performing online lookup on re-enter Closes https://github.com/ppy/osu/issues/34825. Root cause is https://github.com/ppy/osu/blob/24ec43b3b65fa3b164b7713341cd62b1e0dacc2e/osu.Game/Screens/SelectV2/SongSelect.cs#L345-L356 not specifying `(..., true)`, therefore the fetch doesn't happen on enter if song select doesn't change the global beatmap as a side effect of the enter, which is the case on re-entering. --- osu.Game/Screens/SelectV2/SongSelect.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index 947b8f9c7c..ef00064ced 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -653,6 +653,7 @@ namespace osu.Game.Screens.SelectV2 ensurePlayingSelected(); updateBackgroundDim(); + fetchOnlineInfo(); } private void onLeavingScreen() From 04ba5aa57538aaea6cdff7631a04a18839dca4df Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 29 Aug 2025 17:42:14 +0900 Subject: [PATCH 07/38] Move footer to ScreenTestScene --- .../SongSelectV2/SongSelectTestScene.cs | 50 ---------------- osu.Game/Tests/Visual/ScreenTestScene.cs | 60 +++++++++++++++++-- 2 files changed, 55 insertions(+), 55 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs b/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs index b1d1ed8c61..e3b02e5905 100644 --- a/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs +++ b/osu.Game.Tests/Visual/SongSelectV2/SongSelectTestScene.cs @@ -22,8 +22,6 @@ using osu.Game.Overlays.Toolbar; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; -using osu.Game.Screens; -using osu.Game.Screens.Footer; using osu.Game.Screens.Menu; using osu.Game.Screens.Select.Filter; using osu.Game.Screens.SelectV2; @@ -43,9 +41,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 protected Screens.SelectV2.SongSelect SongSelect { get; private set; } = null!; protected BeatmapCarousel Carousel => SongSelect.ChildrenOfType().Single(); - [Cached] - protected readonly ScreenFooter Footer; - [Cached] private readonly OsuLogo logo; @@ -72,10 +67,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 { State = { Value = Visibility.Visible }, }, - Footer = new ScreenFooter - { - BackButtonPressed = () => Stack.CurrentScreen.Exit(), - }, logo = new OsuLogo { Alpha = 0f, @@ -111,14 +102,6 @@ namespace osu.Game.Tests.Visual.SongSelectV2 Add(beatmapStore); } - protected override void LoadComplete() - { - base.LoadComplete(); - - Stack.ScreenPushed += updateFooter; - Stack.ScreenExited += updateFooter; - } - public override void SetUpSteps() { base.SetUpSteps(); @@ -207,38 +190,5 @@ namespace osu.Game.Tests.Visual.SongSelectV2 } protected void WaitForSuspension() => AddUntilStep("wait for not current", () => !SongSelect.AsNonNull().IsCurrentScreen()); - - private void updateFooter(IScreen? _, IScreen? newScreen) - { - if (newScreen is OsuScreen osuScreen && osuScreen.ShowFooter) - { - Footer.Show(); - - if (osuScreen.IsLoaded) - updateFooterButtons(); - else - { - // ensure the current buttons are immediately disabled on screen change (so they can't be pressed). - Footer.SetButtons(Array.Empty()); - - osuScreen.OnLoadComplete += _ => updateFooterButtons(); - } - - void updateFooterButtons() - { - var buttons = osuScreen.CreateFooterButtons(); - - osuScreen.LoadComponentsAgainstScreenDependencies(buttons); - - Footer.SetButtons(buttons); - Footer.Show(); - } - } - else - { - Footer.Hide(); - Footer.SetButtons(Array.Empty()); - } - } } } diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index f780b1a8f8..42199faa4d 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -7,7 +7,9 @@ using osu.Framework.Bindables; using osu.Framework.Development; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Logging; +using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Graphics; using osu.Game.Overlays; @@ -32,7 +34,7 @@ namespace osu.Game.Tests.Visual protected DialogOverlay DialogOverlay { get; private set; } [Cached] - private ScreenFooter footer; + protected ScreenFooter Footer { get; private set; } protected ScreenTestScene() { @@ -43,17 +45,32 @@ namespace osu.Game.Tests.Visual Name = nameof(ScreenTestScene), RelativeSizeAxes = Axes.Both }, - content = new Container { RelativeSizeAxes = Axes.Both }, + new PopoverContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + content = new Container { RelativeSizeAxes = Axes.Both }, + Footer = new ScreenFooter(), + } + }, overlayContent = new Container { RelativeSizeAxes = Axes.Both, Child = DialogOverlay = new DialogOverlay() }, - footer = new ScreenFooter(), }); - Stack.ScreenPushed += (_, newScreen) => Logger.Log($"{nameof(ScreenTestScene)} screen changed → {newScreen}"); - Stack.ScreenExited += (_, newScreen) => Logger.Log($"{nameof(ScreenTestScene)} screen changed ← {newScreen}"); + Stack.ScreenPushed += (oldScreen, newScreen) => + { + updateFooter(oldScreen, newScreen); + Logger.Log($"{nameof(ScreenTestScene)} screen changed → {newScreen}"); + }; + Stack.ScreenExited += (oldScreen, newScreen) => + { + updateFooter(oldScreen, newScreen); + Logger.Log($"{nameof(ScreenTestScene)} screen changed ← {newScreen}"); + }; } protected void LoadScreen(OsuScreen screen) => Stack.Push(screen); @@ -79,6 +96,39 @@ namespace osu.Game.Tests.Visual }); } + private void updateFooter(IScreen? _, IScreen? newScreen) + { + if (newScreen is OsuScreen osuScreen && osuScreen.ShowFooter) + { + Footer.Show(); + + if (osuScreen.IsLoaded) + updateFooterButtons(); + else + { + // ensure the current buttons are immediately disabled on screen change (so they can't be pressed). + Footer.SetButtons(Array.Empty()); + + osuScreen.OnLoadComplete += _ => updateFooterButtons(); + } + + void updateFooterButtons() + { + var buttons = osuScreen.CreateFooterButtons(); + + osuScreen.LoadComponentsAgainstScreenDependencies(buttons); + + Footer.SetButtons(buttons); + Footer.Show(); + } + } + else + { + Footer.Hide(); + Footer.SetButtons(Array.Empty()); + } + } + #region IOverlayManager IBindable IOverlayManager.OverlayActivationMode { get; } = new Bindable(OverlayActivation.All); From 41b8033ebdae5249e86d8b3e5e0fd02d24563b28 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Aug 2025 21:21:47 +0900 Subject: [PATCH 08/38] Adjust interpolation workaround to catch-up slightly smoother --- osu.Android.props | 2 +- osu.Game/Beatmaps/FramedBeatmapClock.cs | 5 ++++- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 40a9b454ce..46d558354e 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -10,7 +10,7 @@ true - + diff --git a/osu.iOS.props b/osu.iOS.props index 7545031cf3..3768550c21 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -17,6 +17,6 @@ -all - + From d6b4c2958dd28f13bee911c8236c254a0359004a Mon Sep 17 00:00:00 2001 From: marvin Date: Sat, 30 Aug 2025 18:48:46 +0200 Subject: [PATCH 09/38] Fix crash when marking previous objects as hit --- .../Screens/Edit/GameplayTest/EditorPlayer.cs | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 02eb38ffa6..9d9202d597 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -14,7 +14,6 @@ using osu.Game.Overlays; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; @@ -55,12 +54,18 @@ namespace osu.Game.Screens.Edit.GameplayTest return masterGameplayClockContainer; } + protected override void LoadAsyncComplete() + { + base.LoadAsyncComplete(); + + preventMissOnPreviousHitObjects(); + } + protected override void LoadComplete() { base.LoadComplete(); markPreviousObjectsHit(); - markVisibleDrawableObjectsHit(); ScoreProcessor.HasCompleted.BindValueChanged(completed => { @@ -111,38 +116,39 @@ namespace osu.Game.Screens.Edit.GameplayTest } } - private void markVisibleDrawableObjectsHit() + private void preventMissOnPreviousHitObjects() { - if (!DrawableRuleset.Playfield.IsLoaded) + void preventMiss(HitObject hitObject) { - Schedule(markVisibleDrawableObjectsHit); - return; - } + if (hitObject.StartTime > editorState.Time) + return; - foreach (var drawableObject in enumerateDrawableObjects(DrawableRuleset.Playfield.AllHitObjects, editorState.Time)) - { - if (drawableObject.Entry == null) - continue; + var drawableObject = DrawableRuleset.Playfield.HitObjectContainer + .AliveObjects + .LastOrDefault(it => it.HitObject == hitObject); + + if (drawableObject?.Entry == null) + return; var result = drawableObject.CreateResult(drawableObject.HitObject.Judgement); result.Type = result.Judgement.MaxResult; drawableObject.Entry.Result = result; } - static IEnumerable enumerateDrawableObjects(IEnumerable drawableObjects, double cutoffTime) + void removeListener() { - foreach (var drawableObject in drawableObjects) + if (!DrawableRuleset.Playfield.IsLoaded) { - foreach (var nested in enumerateDrawableObjects(drawableObject.NestedHitObjects, cutoffTime)) - { - if (nested.HitObject.GetEndTime() < cutoffTime) - yield return nested; - } - - if (drawableObject.HitObject.GetEndTime() < cutoffTime) - yield return drawableObject; + Schedule(removeListener); + return; } + + DrawableRuleset.Playfield.HitObjectUsageBegan -= preventMiss; } + + DrawableRuleset.Playfield.HitObjectUsageBegan += preventMiss; + + Schedule(removeListener); } protected override void PrepareReplay() From b02093505db132e089b269b413db932f99cd849d Mon Sep 17 00:00:00 2001 From: NiyazBiyaz Date: Sun, 31 Aug 2025 17:17:17 +0500 Subject: [PATCH 10/38] Add `OperationInProgress` checking --- osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs | 6 +++++- osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index ae8ad2c01b..5c5b6edcc3 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -97,7 +97,11 @@ namespace osu.Game.Rulesets.Osu.Edit base.LoadComplete(); ScheduleAfterChildren(() => angleInput.TakeFocus()); - angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue }); + angleInput.Current.BindValueChanged(angle => + { + if (rotationHandler.OperationInProgress.Value) + rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue }; + }); rotationHandler.CanRotateAroundSelectionOrigin.BindValueChanged(e => { diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index ac6d9fbb19..86114f1dca 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -140,7 +140,11 @@ namespace osu.Game.Rulesets.Osu.Edit base.LoadComplete(); ScheduleAfterChildren(() => scaleInput.TakeFocus()); - scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue }); + scaleInput.Current.BindValueChanged(scale => + { + if (scaleHandler.OperationInProgress.Value) + scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue }; + }); xCheckBox.Current.BindValueChanged(_ => { From 12430ce464326d468e13c0564e623854687ab61d Mon Sep 17 00:00:00 2001 From: NiyazBiyaz Date: Mon, 1 Sep 2025 13:55:29 +0500 Subject: [PATCH 11/38] Move guard to `scale/rotationInfo` --- osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs | 10 +++++----- osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index 5c5b6edcc3..ba67bf1f2d 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -97,11 +97,7 @@ namespace osu.Game.Rulesets.Osu.Edit base.LoadComplete(); ScheduleAfterChildren(() => angleInput.TakeFocus()); - angleInput.Current.BindValueChanged(angle => - { - if (rotationHandler.OperationInProgress.Value) - rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue }; - }); + angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue }); rotationHandler.CanRotateAroundSelectionOrigin.BindValueChanged(e => { @@ -161,6 +157,10 @@ namespace osu.Game.Rulesets.Osu.Edit rotationInfo.BindValueChanged(rotation => { + // can happen if the popover is dismessed by a keyboard key press while dragging UI controls + if (!rotationHandler.OperationInProgress.Value) + return; + rotationHandler.Update(rotation.NewValue.Degrees, getOriginPosition(rotation.NewValue)); }); } diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs index 86114f1dca..ca4a99b9cd 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseScalePopover.cs @@ -140,11 +140,7 @@ namespace osu.Game.Rulesets.Osu.Edit base.LoadComplete(); ScheduleAfterChildren(() => scaleInput.TakeFocus()); - scaleInput.Current.BindValueChanged(scale => - { - if (scaleHandler.OperationInProgress.Value) - scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue }; - }); + scaleInput.Current.BindValueChanged(scale => scaleInfo.Value = scaleInfo.Value with { Scale = scale.NewValue }); xCheckBox.Current.BindValueChanged(_ => { @@ -224,6 +220,10 @@ namespace osu.Game.Rulesets.Osu.Edit scaleInfo.BindValueChanged(scale => { + // can happen if the popover is dismissed by a keyboard key press while dragging UI controls + if (!scaleHandler.OperationInProgress.Value) + return; + var newScale = new Vector2(scale.NewValue.Scale, scale.NewValue.Scale); scaleHandler.Update(newScale, getOriginPosition(scale.NewValue), getAdjustAxis(scale.NewValue), getRotation(scale.NewValue)); }); From 677c008b4d5ab684405731d85eb10e7859e1bbd5 Mon Sep 17 00:00:00 2001 From: NiyazBiyaz Date: Mon, 1 Sep 2025 13:57:14 +0500 Subject: [PATCH 12/38] Fix movement via slider while popover closes --- osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs index 04d6afc925..a3282734be 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs @@ -176,6 +176,11 @@ namespace osu.Game.Rulesets.Osu.Edit private void applyPosition() { + // can happen if popover disabled by a keyboard key press while dragging UI controls + // it doesn't cause a crash, but it looks wrong + if (!editorBeatmap.TransactionActive) + return; + editorBeatmap.PerformOnSelection(ho => { if (!initialPositions.TryGetValue(ho, out var initialPosition)) From 0021434a62ea4aea65281957b9ac5dff4ba4121f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 1 Sep 2025 17:54:24 +0900 Subject: [PATCH 13/38] Fix cancellation token not actually being used --- osu.Game/Screens/SelectV2/SongSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/SelectV2/SongSelect.cs b/osu.Game/Screens/SelectV2/SongSelect.cs index ef00064ced..b2dc8404e4 100644 --- a/osu.Game/Screens/SelectV2/SongSelect.cs +++ b/osu.Game/Screens/SelectV2/SongSelect.cs @@ -1011,7 +1011,7 @@ namespace osu.Game.Screens.SelectV2 lastLookupResult.Value = BeatmapSetLookupResult.InProgress(); onlineLookupCancellation = new CancellationTokenSource(); - currentOnlineLookup = onlineLookupSource.GetBeatmapSetAsync(beatmapSetInfo.OnlineID); + currentOnlineLookup = onlineLookupSource.GetBeatmapSetAsync(beatmapSetInfo.OnlineID, onlineLookupCancellation.Token); currentOnlineLookup.ContinueWith(t => { if (t.IsCompletedSuccessfully) From 9d0043d03bf74b5f0f86aa6018c2d3cd212e0cde Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 1 Sep 2025 17:58:41 +0900 Subject: [PATCH 14/38] Cancel underlying web request on local cancellation of lookup request --- osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs b/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs index 486dfbe255..832095058a 100644 --- a/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs +++ b/osu.Game/Screens/SelectV2/RealmPopulatingOnlineLookupSource.cs @@ -43,6 +43,8 @@ namespace osu.Game.Screens.SelectV2 var request = new GetBeatmapSetRequest(id); var tcs = new TaskCompletionSource(); + token.Register(() => request.Cancel()); + // async request success callback is a bit of a dangerous game, but there's some reasoning for it. // - don't really want to use `IAPIAccess.PerformAsync()` because we still want to respect request queueing & online status checks // - we want the realm write here to be async because it is known to be slow for some users with large beatmap collections From 9827f9f189f2e55dab9ad35106af4ac595d7afff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 1 Sep 2025 11:10:30 +0200 Subject: [PATCH 15/38] Recursively update hit results for nested drawables --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 9d9202d597..66e04d1c09 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -14,6 +14,7 @@ using osu.Game.Overlays; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; @@ -127,12 +128,20 @@ namespace osu.Game.Screens.Edit.GameplayTest .AliveObjects .LastOrDefault(it => it.HitObject == hitObject); + preventMissOnDrawable(drawableObject); + } + + void preventMissOnDrawable(DrawableHitObject? drawableObject) + { if (drawableObject?.Entry == null) return; var result = drawableObject.CreateResult(drawableObject.HitObject.Judgement); result.Type = result.Judgement.MaxResult; drawableObject.Entry.Result = result; + + foreach (var nested in drawableObject.NestedHitObjects) + preventMissOnDrawable(nested); } void removeListener() From da7e256302f5045a50ca0d7c49b9fe8c6038f729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 1 Sep 2025 11:11:26 +0200 Subject: [PATCH 16/38] Move `markPreviousObjectsHit` into `LoadAsyncComplete` --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 66e04d1c09..8e0a71ddd3 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -60,14 +60,13 @@ namespace osu.Game.Screens.Edit.GameplayTest base.LoadAsyncComplete(); preventMissOnPreviousHitObjects(); + markPreviousObjectsHit(); } protected override void LoadComplete() { base.LoadComplete(); - markPreviousObjectsHit(); - ScoreProcessor.HasCompleted.BindValueChanged(completed => { if (completed.NewValue) From 689cc27e6806c236b14ff5eab067df5f2236eaaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 1 Sep 2025 11:12:19 +0200 Subject: [PATCH 17/38] Prevent npr in tests due to drawable ruleset not being available --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 8e0a71ddd3..5f0139a100 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -59,8 +59,11 @@ namespace osu.Game.Screens.Edit.GameplayTest { base.LoadAsyncComplete(); - preventMissOnPreviousHitObjects(); - markPreviousObjectsHit(); + if (DrawableRuleset != null) + { + preventMissOnPreviousHitObjects(); + markPreviousObjectsHit(); + } } protected override void LoadComplete() From e2d661736e56f2643bfd982f96799c1f87e06322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 1 Sep 2025 12:01:36 +0200 Subject: [PATCH 18/38] Fix typos --- osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs | 2 +- osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs index a3282734be..f3739ab445 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseMovementPopover.cs @@ -176,7 +176,7 @@ namespace osu.Game.Rulesets.Osu.Edit private void applyPosition() { - // can happen if popover disabled by a keyboard key press while dragging UI controls + // can happen if popover is dismissed by a keyboard key press while dragging UI controls // it doesn't cause a crash, but it looks wrong if (!editorBeatmap.TransactionActive) return; diff --git a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs index ba67bf1f2d..e2cde1a325 100644 --- a/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs +++ b/osu.Game.Rulesets.Osu/Edit/PreciseRotationPopover.cs @@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Osu.Edit rotationInfo.BindValueChanged(rotation => { - // can happen if the popover is dismessed by a keyboard key press while dragging UI controls + // can happen if the popover is dismissed by a keyboard key press while dragging UI controls if (!rotationHandler.OperationInProgress.Value) return; From 060854f23a2029692eb0944b7542c5fcdb573e32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 1 Sep 2025 12:09:33 +0200 Subject: [PATCH 19/38] Revert moving `markPreviousObjectsHit` into `LoadAsyncComplete` Running that there caused a test failure due to modifying drawables' transforms outside the update thread --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 5f0139a100..b99c0afdeb 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -60,16 +60,17 @@ namespace osu.Game.Screens.Edit.GameplayTest base.LoadAsyncComplete(); if (DrawableRuleset != null) - { preventMissOnPreviousHitObjects(); - markPreviousObjectsHit(); - } } protected override void LoadComplete() { base.LoadComplete(); + // this will notify components such as the skin's combo counter, which needs to happen on the update thread + // and therefore can't happen alongside `preventMissOnPreviousHitObjects()` in `LoadAsyncComplete()` + markPreviousObjectsHit(); + ScoreProcessor.HasCompleted.BindValueChanged(completed => { if (completed.NewValue) From 5079a53cca81691e768e5e2e5ccdaaa2183bc439 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 1 Sep 2025 19:37:35 +0900 Subject: [PATCH 20/38] Add more panel types to `TestSceneRoomPanel` --- .../Visual/Multiplayer/TestSceneRoomPanel.cs | 71 +++++++++++++++++-- 1 file changed, 64 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs index 58eb0f1ea1..6eb356d28f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs @@ -19,6 +19,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Tests.Beatmaps; using osuTK; @@ -38,10 +39,11 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create rooms", () => { - PlaylistItem item1 = new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo) + PlaylistItem item1 = new PlaylistItem(new APIBeatmap { - BeatmapInfo = { StarRating = 2.5 } - }.BeatmapInfo); + OnlineBeatmapSetID = 173612, + OnlineID = 502132, + }); PlaylistItem item2 = new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo) { @@ -72,6 +74,14 @@ namespace osu.Game.Tests.Visual.Multiplayer Spacing = new Vector2(10), Children = new Drawable[] { + createMultiplayerPanel(new Room + { + Name = "Multiplayer room", + EndDate = DateTimeOffset.Now.AddDays(1), + Type = MatchType.HeadToHead, + Playlist = [item1], + CurrentPlaylistItem = item1 + }), createLoungeRoom(new Room { Name = "Multiplayer room", @@ -98,6 +108,14 @@ namespace osu.Game.Tests.Visual.Multiplayer Playlist = [item3], CurrentPlaylistItem = item3 }), + createPlaylistRoomPanel(new Room + { + Name = "Playlist room with multiple beatmaps", + Status = RoomStatus.Playing, + EndDate = DateTimeOffset.Now.AddDays(1), + Playlist = [item1, item2], + CurrentPlaylistItem = item1 + }), createLoungeRoom(new Room { Name = "Playlist room with multiple beatmaps", @@ -131,8 +149,10 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddUntilStep("wait for panel load", () => rooms.Count, () => Is.EqualTo(8)); - AddUntilStep("\"currently playing\" room count correct", () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Currently playing", StringComparison.Ordinal)), () => Is.EqualTo(3)); - AddUntilStep("\"ready to play\" room count correct", () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)), () => Is.EqualTo(4)); + AddUntilStep("\"currently playing\" room count correct", + () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Currently playing", StringComparison.Ordinal)), () => Is.EqualTo(3)); + AddUntilStep("\"ready to play\" room count correct", () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)), + () => Is.EqualTo(4)); } [Test] @@ -207,7 +227,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { new MultiplayerRoomPanel(new Room { - Name = "This room has a very very long title enough to make the external link button reach the participants list on the right side unless the test window is very wide, at which point I don't know, hi.", + Name = + "This room has a very very long title enough to make the external link button reach the participants list on the right side unless the test window is very wide, at which point I don't know, hi.", QueueMode = QueueMode.HostOnly, Type = MatchType.HeadToHead, RoomID = 1337, @@ -231,7 +252,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { new MultiplayerRoomPanel(room = new Room { - Name = "This room has a very very long title enough to make the external link button reach the participants list on the right side unless the test window is very wide, at which point I don't know, hi.", + Name = + "This room has a very very long title enough to make the external link button reach the participants list on the right side unless the test window is very wide, at which point I don't know, hi.", QueueMode = QueueMode.HostOnly, Type = MatchType.HeadToHead, }), @@ -243,6 +265,41 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("clear room ID", () => room.RoomID = null); } + private RoomPanel createPlaylistRoomPanel(Room room) + { + room.Host ??= new APIUser { Username = "peppy", Id = 2 }; + + if (room.RecentParticipants.Count == 0) + { + room.RecentParticipants = Enumerable.Range(0, 20).Select(i => new APIUser + { + Id = i, + Username = $"User {i}" + }).ToArray(); + } + + return new PlaylistsRoomPanel(room) + { + SelectedItem = new Bindable(room.CurrentPlaylistItem), + }; + } + + private RoomPanel createMultiplayerPanel(Room room) + { + room.Host ??= new APIUser { Username = "peppy", Id = 2 }; + + if (room.RecentParticipants.Count == 0) + { + room.RecentParticipants = Enumerable.Range(0, 20).Select(i => new APIUser + { + Id = i, + Username = $"User {i}" + }).ToArray(); + } + + return new MultiplayerRoomPanel(room); + } + private RoomPanel createLoungeRoom(Room room) { room.Host ??= new APIUser { Username = "peppy", Id = 2 }; From 209ba76b219f5c5357938816c6c24539ea79c2ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 1 Sep 2025 19:37:47 +0900 Subject: [PATCH 21/38] Reduce size of online play screen's header --- osu.Game/Screens/OnlinePlay/Header.cs | 2 +- osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Header.cs b/osu.Game/Screens/OnlinePlay/Header.cs index 860042fd37..825f809397 100644 --- a/osu.Game/Screens/OnlinePlay/Header.cs +++ b/osu.Game/Screens/OnlinePlay/Header.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.OnlinePlay { public partial class Header : Container { - public const float HEIGHT = 80; + public const float HEIGHT = 50; private readonly ScreenStack? stack; private readonly MultiHeaderTitle title; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index a4e808ff76..b4b039501f 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -173,7 +173,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge { d.Anchor = Anchor.BottomLeft; d.Origin = Anchor.BottomLeft; - d.Size = new Vector2(150, 37.5f); + d.Size = new Vector2(150, 30f); d.Action = () => Open(); })), new FillFlowContainer From 659480fa3f137007873977f602b6359c12785a3e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 1 Sep 2025 19:38:18 +0900 Subject: [PATCH 22/38] Adjust sizing of room panels and other elements to make things fit better on mobile layouts --- .../DrawableRoomParticipantsList.cs | 20 ++++++------- .../Lounge/Components/RoomListing.cs | 6 +--- .../OnlinePlay/Lounge/Components/RoomPanel.cs | 28 +++++++++---------- .../Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- 4 files changed, 25 insertions(+), 31 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs index 5bcc974c26..135b2b4db2 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs @@ -23,10 +23,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { public partial class DrawableRoomParticipantsList : CompositeDrawable { - public const float SHEAR_WIDTH = 12f; - private const float avatar_size = 36; - private const float height = 60f; - private static readonly Vector2 shear = new Vector2(SHEAR_WIDTH / height, 0); + private const float avatar_size = 30; + private const float height = 40f; private readonly Room room; @@ -54,7 +52,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components RelativeSizeAxes = Axes.Both, Masking = true, CornerRadius = 10, - Shear = shear, + Shear = OsuGame.SHEAR, Child = new Box { RelativeSizeAxes = Axes.Both, @@ -71,10 +69,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { RelativeSizeAxes = Axes.Y, AutoSizeAxes = Axes.X, - Spacing = new Vector2(8), + Spacing = new Vector2(4), Padding = new MarginPadding { - Left = 8, + Left = 4, Right = 16 }, Children = new Drawable[] @@ -84,7 +82,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, - hostText = new LinkFlowContainer + hostText = new LinkFlowContainer(s => s.Font = OsuFont.Style.Caption2) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -103,7 +101,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components RelativeSizeAxes = Axes.Both, Masking = true, CornerRadius = 10, - Shear = shear, + Shear = OsuGame.SHEAR, Child = new Box { RelativeSizeAxes = Axes.Both, @@ -128,12 +126,12 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Size = new Vector2(16), + Size = new Vector2(12), Icon = FontAwesome.Solid.User, }, totalCount = new OsuSpriteText { - Font = OsuFont.Default.With(weight: FontWeight.Bold), + Font = OsuFont.Style.Caption2.With(weight: FontWeight.Bold), Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, }, diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs index b93d26880d..f04de97f9b 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomListing.cs @@ -45,8 +45,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private readonly ScrollContainer scroll; private readonly FillFlowContainer roomFlow; - private const float display_scale = 0.8f; - // handle deselection public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; @@ -58,7 +56,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components RelativeSizeAxes = Axes.Both, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Width = display_scale, + Width = 0.8f, ScrollbarOverlapsContent = false, Padding = new MarginPadding { Right = 5 }, Child = new OsuContextMenuContainer @@ -188,8 +186,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components SelectedRoom = selectedRoom, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Scale = new Vector2(display_scale), - Width = 1 / display_scale, }; roomFlow.Add(drawableRoom); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs index 258c9c3a97..fe03fca4b8 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomPanel.cs @@ -31,7 +31,6 @@ using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Components; using osuTK; -using osuTK.Graphics; using Container = osu.Framework.Graphics.Containers.Container; namespace osu.Game.Screens.OnlinePlay.Lounge.Components @@ -39,7 +38,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components public abstract partial class RoomPanel : CompositeDrawable, IHasContextMenu { protected const float CORNER_RADIUS = 10; - private const float height = 100; + private const float height = 80; [Resolved] private IAPIProvider api { get; set; } = null!; @@ -80,12 +79,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Masking = true; CornerRadius = CORNER_RADIUS; - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(40), - Radius = 5, - }; } [BackgroundDependencyLoader] @@ -99,6 +92,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components AutoSizeAxes = Axes.X }; + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = colourProvider.Background6.Opacity(0.4f), + Radius = 4, + }; + InternalChildren = new Drawable[] { // This resolves internal 1px gaps due to applying the (parenting) corner radius and masking across multiple filling background sprites. @@ -118,7 +118,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Name = @"Room content", RelativeSizeAxes = Axes.Both, // This negative padding resolves 1px gaps between this background and the background above. - Padding = new MarginPadding { Left = 20, Vertical = -0.5f }, + Padding = new MarginPadding { Left = 10, Vertical = -0.5f }, Child = new Container { RelativeSizeAxes = Axes.Both, @@ -158,8 +158,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { - Left = 20, - Right = DrawableRoomParticipantsList.SHEAR_WIDTH, + Left = 10, + Right = 10, Vertical = 5 }, Children = new Drawable[] @@ -516,12 +516,12 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { statusText = new OsuSpriteText { - Font = OsuFont.Default.With(size: 16), + Font = OsuFont.Style.Caption2, Colour = colours.Lime1 }, beatmapText = new LinkFlowContainer(s => { - s.Font = OsuFont.Default.With(size: 16); + s.Font = OsuFont.Style.Caption2; s.Colour = colours.Lime1; }) { @@ -636,7 +636,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Font = OsuFont.GetFont(size: 28), + Font = OsuFont.Style.Heading2, }, linkButton = new ExternalLinkButton { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 689a8df12f..bbac86fd2d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -280,7 +280,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new AddItemButton { RelativeSizeAxes = Axes.X, - Height = 40, + Height = 30, Text = "Add item", Action = () => ShowSongSelect() }, From cf471066bfac41a007c3d5277355c9fcfe01c918 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 1 Sep 2025 19:38:27 +0900 Subject: [PATCH 23/38] Add basic spacing between participants in list --- .../OnlinePlay/Multiplayer/Participants/ParticipantsList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs index b553fcc9cd..7429fc817c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs @@ -22,7 +22,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants private MultiplayerClient client { get; set; } = null!; public ParticipantsList() - : base(ParticipantPanel.HEIGHT, initialPoolSize: 20) + : base(ParticipantPanel.HEIGHT + 1, initialPoolSize: 20) { } From 385529ec7813d49beba1692328100ca1e2f7745f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 1 Sep 2025 13:05:22 +0200 Subject: [PATCH 24/38] Fix mismatch in cutoff time check between `preventMissOnPreviousHitObjects` and `markPreviousObjectsHit` --- .../Screens/Edit/GameplayTest/EditorPlayer.cs | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index b99c0afdeb..589ce34450 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -124,27 +124,28 @@ namespace osu.Game.Screens.Edit.GameplayTest { void preventMiss(HitObject hitObject) { - if (hitObject.StartTime > editorState.Time) - return; - var drawableObject = DrawableRuleset.Playfield.HitObjectContainer .AliveObjects .LastOrDefault(it => it.HitObject == hitObject); - preventMissOnDrawable(drawableObject); + if (drawableObject != null) + preventMissOnDrawable(drawableObject); } - void preventMissOnDrawable(DrawableHitObject? drawableObject) + void preventMissOnDrawable(DrawableHitObject drawableObject) { - if (drawableObject?.Entry == null) + if (drawableObject.Entry == null) return; - var result = drawableObject.CreateResult(drawableObject.HitObject.Judgement); - result.Type = result.Judgement.MaxResult; - drawableObject.Entry.Result = result; - foreach (var nested in drawableObject.NestedHitObjects) preventMissOnDrawable(nested); + + if (drawableObject.HitObject.GetEndTime() < editorState.Time) + { + var result = drawableObject.CreateResult(drawableObject.HitObject.Judgement); + result.Type = result.Judgement.MaxResult; + drawableObject.Entry.Result = result; + } } void removeListener() From ffb6ae206682218d93f451d102391753c5925e10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20Sch=C3=BCrz?= Date: Mon, 1 Sep 2025 13:13:28 +0200 Subject: [PATCH 25/38] Move null check after loop over nested hitobjects --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 589ce34450..90996fda6f 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -134,13 +134,10 @@ namespace osu.Game.Screens.Edit.GameplayTest void preventMissOnDrawable(DrawableHitObject drawableObject) { - if (drawableObject.Entry == null) - return; - foreach (var nested in drawableObject.NestedHitObjects) preventMissOnDrawable(nested); - if (drawableObject.HitObject.GetEndTime() < editorState.Time) + if (drawableObject.Entry != null && drawableObject.HitObject.GetEndTime() < editorState.Time) { var result = drawableObject.CreateResult(drawableObject.HitObject.Judgement); result.Type = result.Judgement.MaxResult; From a008a66fb27e55e06fc84665b2e3bf842c46afad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 1 Sep 2025 13:50:42 +0200 Subject: [PATCH 26/38] Fix test --- osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs index 6eb356d28f..aa9dddae4d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomPanel.cs @@ -148,11 +148,11 @@ namespace osu.Game.Tests.Visual.Multiplayer }; }); - AddUntilStep("wait for panel load", () => rooms.Count, () => Is.EqualTo(8)); + AddUntilStep("wait for panel load", () => rooms.Count, () => Is.EqualTo(10)); AddUntilStep("\"currently playing\" room count correct", - () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Currently playing", StringComparison.Ordinal)), () => Is.EqualTo(3)); + () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Currently playing", StringComparison.Ordinal)), () => Is.EqualTo(4)); AddUntilStep("\"ready to play\" room count correct", () => rooms.ChildrenOfType().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)), - () => Is.EqualTo(4)); + () => Is.EqualTo(5)); } [Test] From cac136d3c6026cf2bb8b8e35d736520e5ebdc67c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 2 Sep 2025 09:21:31 +0900 Subject: [PATCH 27/38] Fix editor memory leak --- .../Compose/Components/Timeline/SamplePointPiece.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 5e8637c1ac..cdd2f52dab 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -16,7 +16,6 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Audio; -using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; @@ -57,9 +56,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected virtual double GetTime() => HitObject is IHasRepeats r ? HitObject.StartTime + r.Duration / r.SpanCount() / 2 : HitObject.StartTime; [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load() { - HitObject.DefaultsApplied += _ => updateText(); Label.AllowMultiline = false; LabelContainer.AutoSizeAxes = Axes.None; updateText(); @@ -74,6 +72,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { base.LoadComplete(); + HitObject.DefaultsApplied += onDefaultsApplied; + if (timelineBlueprintContainer != null) contracted.BindTo(timelineBlueprintContainer.SamplePointContracted); @@ -96,12 +96,19 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline FinishTransforms(); } + private void onDefaultsApplied(HitObject hitObject) + { + updateText(); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); if (editor != null) editor.ShowSampleEditPopoverRequested -= onShowSampleEditPopoverRequested; + + HitObject.DefaultsApplied -= onDefaultsApplied; } private void onShowSampleEditPopoverRequested(double time) From f9e89afe03c13e544656e31071fc86048ebba211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krystian=20Ptach-=C5=BBurakowski?= <69014595+kptach@users.noreply.github.com> Date: Tue, 2 Sep 2025 03:10:58 +0200 Subject: [PATCH 28/38] Add increase visibility setting for taiko hidden (#34879) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- .../Mods/TestSceneTaikoModHidden.cs | 104 ++++++++++++++++++ .../Mods/TaikoModHidden.cs | 2 +- 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModHidden.cs b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModHidden.cs index e6d5c51902..5336ea604e 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Mods/TestSceneTaikoModHidden.cs @@ -5,10 +5,13 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Configuration; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; namespace osu.Game.Rulesets.Taiko.Tests.Mods { @@ -69,5 +72,106 @@ namespace osu.Game.Rulesets.Taiko.Tests.Mods }, }); } + + [Test] + public void TestIncreasedVisibilityOnFirstObject() + { + bool firstHitNeverFadedOut = true; + AddStep("enable increased visibility", () => LocalConfig.SetValue(OsuSetting.IncreaseFirstObjectVisibility, true)); + CreateModTest(new ModTestData + { + Mod = new TaikoModHidden(), + Autoplay = true, + PassCondition = () => + { + var firstHit = this.ChildrenOfType().FirstOrDefault(h => h.HitObject.StartTime == 100); + + if (firstHit?.Alpha < 1 && !firstHit.IsHit) + firstHitNeverFadedOut = false; + + return firstHitNeverFadedOut && checkAllMaxResultJudgements(2).Invoke(); + }, + CreateBeatmap = () => + { + var beatmap = new Beatmap + { + HitObjects = new List + { + new Hit + { + Type = HitType.Rim, + StartTime = 100, + }, + new Hit + { + Type = HitType.Centre, + StartTime = 200, + }, + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty + { + SliderTickRate = 4, + OverallDifficulty = 0, + }, + Ruleset = new TaikoRuleset().RulesetInfo + }, + }; + + beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f }); + return beatmap; + }, + }); + } + + [Test] + public void TestNoIncreasedVisibilityOnFirstObject() + { + bool firstHitFadedOut = true; + AddStep("enable increased visibility", () => LocalConfig.SetValue(OsuSetting.IncreaseFirstObjectVisibility, false)); + CreateModTest(new ModTestData + { + Mod = new TaikoModHidden(), + Autoplay = true, + PassCondition = () => + { + var firstHit = this.ChildrenOfType().FirstOrDefault(h => h.HitObject.StartTime == 100); + firstHitFadedOut |= firstHit?.IsHit == false && firstHit.Alpha < 1; + return firstHitFadedOut && checkAllMaxResultJudgements(2).Invoke(); + }, + CreateBeatmap = () => + { + var beatmap = new Beatmap + { + HitObjects = new List + { + new Hit + { + Type = HitType.Rim, + StartTime = 100, + }, + new Hit + { + Type = HitType.Centre, + StartTime = 200, + }, + }, + BeatmapInfo = + { + Difficulty = new BeatmapDifficulty + { + SliderTickRate = 4, + OverallDifficulty = 0, + }, + Ruleset = new TaikoRuleset().RulesetInfo + }, + }; + + beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f }); + return beatmap; + }, + }); + } } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs index 2c3b4a8d18..8b6fb71d51 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Mods protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) { - ApplyNormalVisibilityState(hitObject, state); + // intentional no-op } protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) From b0dcd06b383e1585bc9b281a2fc62314f019e56c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Sep 2025 08:02:32 +0200 Subject: [PATCH 29/38] Add one more comment --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 90996fda6f..0b6c9960c8 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -59,6 +59,8 @@ namespace osu.Game.Screens.Edit.GameplayTest { base.LoadAsyncComplete(); + // `preventMissOnPreviousHitObjects()` needs to be called to install its hooks before drawable hit objects get the chance to run update logic, + // because it will not work otherwise due to being too late (various effects of the objects getting missed will have already taken place). if (DrawableRuleset != null) preventMissOnPreviousHitObjects(); } From 903d91b69784dd537edaa17bd0a178c7eb7d5a53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Sep 2025 08:05:37 +0200 Subject: [PATCH 30/38] Use `SingleOrDefault()` instead of `LastOrDefault()` `LastOrDefault()` is arbitrary, and I hope this doesn't matter for anything, because if it does, then that's utterly *horrifying*. --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 0b6c9960c8..525f6f62ad 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -128,7 +128,7 @@ namespace osu.Game.Screens.Edit.GameplayTest { var drawableObject = DrawableRuleset.Playfield.HitObjectContainer .AliveObjects - .LastOrDefault(it => it.HitObject == hitObject); + .SingleOrDefault(it => it.HitObject == hitObject); if (drawableObject != null) preventMissOnDrawable(drawableObject); From a8ef57ad0a3fc6e95a0b2dae0076ee2b2b4d6d91 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Sep 2025 17:36:08 +0900 Subject: [PATCH 31/38] Revert "Adjust bass invalid data threshold" This reverts commit ddce11fbc8e9aabbb2e4e943b2901ff701987685. --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 329a41ef28..7f29ed3703 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.UI // // In testing this triggers *very* rarely even when set to super low values (10 ms). The cases we're worried about involve multi-second jumps. // A difference of more than 500 ms seems like a sane number we should never exceed. - if (!allowReferenceClockSeeks && Math.Abs(proposedTime - referenceClock.CurrentTime) > 1500) + if (!allowReferenceClockSeeks && Math.Abs(proposedTime - referenceClock.CurrentTime) > 500) { if (invalidBassTimeLogCount < 10) { From 677beb4251b0017ef2c2c2b8e56512988d328705 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Sep 2025 17:37:33 +0900 Subject: [PATCH 32/38] Fix gameplay freezing on stutter frames / long load times Closes https://github.com/ppy/osu/issues/34732. May hotfix for this one. --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 7f29ed3703..892f4acb78 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -161,7 +161,9 @@ namespace osu.Game.Rulesets.UI // // In testing this triggers *very* rarely even when set to super low values (10 ms). The cases we're worried about involve multi-second jumps. // A difference of more than 500 ms seems like a sane number we should never exceed. - if (!allowReferenceClockSeeks && Math.Abs(proposedTime - referenceClock.CurrentTime) > 500) + // + // Double-checking against the parent clock ensures we don't accidentally freeze time when the game stutters due to a long running frame. + if (!allowReferenceClockSeeks && Math.Abs(proposedTime - referenceClock.CurrentTime) > 500 && parentGameplayClock?.ElapsedFrameTime <= 500) { if (invalidBassTimeLogCount < 10) { From 1519084f72bd34d3af83cfbcccf728225a79b791 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 2 Sep 2025 17:57:15 +0900 Subject: [PATCH 33/38] Eagerly clear the request queue on join --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 1dfa3c0cfb..745e773512 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -202,6 +202,7 @@ namespace osu.Game.Online.Multiplayer await joinOrLeaveTaskChain.Add(async () => { + await runOnUpdateThreadAsync(() => pendingRequests.Clear(), cancellationSource.Token).ConfigureAwait(false); var multiplayerRoom = await CreateRoomInternal(new MultiplayerRoom(room)).ConfigureAwait(false); await setupJoinedRoom(room, multiplayerRoom, cancellationSource.Token).ConfigureAwait(false); }, cancellationSource.Token).ConfigureAwait(false); @@ -225,6 +226,7 @@ namespace osu.Game.Online.Multiplayer await joinOrLeaveTaskChain.Add(async () => { + await runOnUpdateThreadAsync(() => pendingRequests.Clear(), cancellationSource.Token).ConfigureAwait(false); var multiplayerRoom = await JoinRoomInternal(room.RoomID.Value, password ?? room.Password).ConfigureAwait(false); await setupJoinedRoom(room, multiplayerRoom, cancellationSource.Token).ConfigureAwait(false); }, cancellationSource.Token).ConfigureAwait(false); From 4ed72efeae45ee8788c42f99f8b5e7b1f1bf4503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Sep 2025 11:22:41 +0200 Subject: [PATCH 34/38] Use better guard (and reword subsequent comment) Co-authored-by: Dean Herbert --- osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs index 525f6f62ad..eedde8b7a4 100644 --- a/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs +++ b/osu.Game/Screens/Edit/GameplayTest/EditorPlayer.cs @@ -59,10 +59,12 @@ namespace osu.Game.Screens.Edit.GameplayTest { base.LoadAsyncComplete(); - // `preventMissOnPreviousHitObjects()` needs to be called to install its hooks before drawable hit objects get the chance to run update logic, + if (!LoadedBeatmapSuccessfully) + return; + + // This hack needs to be called to install its hooks before drawable hit objects get the chance to run update logic, // because it will not work otherwise due to being too late (various effects of the objects getting missed will have already taken place). - if (DrawableRuleset != null) - preventMissOnPreviousHitObjects(); + preventMissOnPreviousHitObjects(); } protected override void LoadComplete() From bee6c32b83de4b4a5ef1b28a7ad880b7f13146aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Sep 2025 21:39:12 +0900 Subject: [PATCH 35/38] Change bass workaround fix to use game clock intead of another-audio-clock paper trail: https://github.com/ppy/osu/pull/34890#issuecomment-3244549790 --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 892f4acb78..ffefea570e 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -63,6 +63,9 @@ namespace osu.Game.Rulesets.UI /// private readonly FramedClock framedClock; + [Resolved] + private OsuGame game { get; set; } = null!; + private readonly Stopwatch stopwatch = new Stopwatch(); /// @@ -163,7 +166,7 @@ namespace osu.Game.Rulesets.UI // A difference of more than 500 ms seems like a sane number we should never exceed. // // Double-checking against the parent clock ensures we don't accidentally freeze time when the game stutters due to a long running frame. - if (!allowReferenceClockSeeks && Math.Abs(proposedTime - referenceClock.CurrentTime) > 500 && parentGameplayClock?.ElapsedFrameTime <= 500) + if (!allowReferenceClockSeeks && Math.Abs(proposedTime - referenceClock.CurrentTime) > 500 && game.Clock.ElapsedFrameTime <= 500) { if (invalidBassTimeLogCount < 10) { From a1105ba16fc660bf5a618a95cdedbd773d9e066d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Sep 2025 21:40:52 +0900 Subject: [PATCH 36/38] Make `OsuGame` dependency optional for sanity --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index ffefea570e..990c1c839b 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.UI private readonly FramedClock framedClock; [Resolved] - private OsuGame game { get; set; } = null!; + private OsuGame? game { get; set; } private readonly Stopwatch stopwatch = new Stopwatch(); @@ -166,7 +166,7 @@ namespace osu.Game.Rulesets.UI // A difference of more than 500 ms seems like a sane number we should never exceed. // // Double-checking against the parent clock ensures we don't accidentally freeze time when the game stutters due to a long running frame. - if (!allowReferenceClockSeeks && Math.Abs(proposedTime - referenceClock.CurrentTime) > 500 && game.Clock.ElapsedFrameTime <= 500) + if (!allowReferenceClockSeeks && Math.Abs(proposedTime - referenceClock.CurrentTime) > 500 && game?.Clock.ElapsedFrameTime <= 500) { if (invalidBassTimeLogCount < 10) { From 95c72524677527dfe6b3db4ed62723804b3ade21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Sep 2025 14:43:41 +0200 Subject: [PATCH 37/38] Add failing test case --- .../Database/BeatmapImporterTests.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/osu.Game.Tests/Database/BeatmapImporterTests.cs b/osu.Game.Tests/Database/BeatmapImporterTests.cs index 38746f2567..f3ca665380 100644 --- a/osu.Game.Tests/Database/BeatmapImporterTests.cs +++ b/osu.Game.Tests/Database/BeatmapImporterTests.cs @@ -1018,6 +1018,49 @@ namespace osu.Game.Tests.Database }); } + [Test] + public void TestBeatmapFilesInNestedDirectoriesAreIgnored() + { + RunTestWithRealmAsync(async (realm, storage) => + { + var importer = new BeatmapImporter(storage, realm); + using var store = new RealmRulesetStore(realm, storage); + + string? temp = TestResources.GetTestBeatmapForImport(); + + string extractedFolder = $"{temp}_extracted"; + Directory.CreateDirectory(extractedFolder); + + try + { + using (var zip = ZipArchive.Open(temp)) + zip.WriteToDirectory(extractedFolder); + + var subdirectory = Directory.CreateDirectory(Path.Combine(extractedFolder, "subdir")); + string modifiedCopyPath = Path.Combine(subdirectory.FullName, "duplicate.osu"); + File.Copy(Directory.GetFiles(extractedFolder, "*.osu").First(), modifiedCopyPath); + + using (var stream = File.OpenWrite(modifiedCopyPath)) + using (var textWriter = new StreamWriter(stream)) + await textWriter.WriteLineAsync("# adding a comment so that the hashes are different"); + + using (var zip = ZipArchive.Create()) + { + zip.AddAllFromDirectory(extractedFolder); + zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate)); + } + + await importer.Import(temp); + + EnsureLoaded(realm.Realm); + } + finally + { + Directory.Delete(extractedFolder, true); + } + }); + } + [Test] public void TestImportNestedStructure() { From 79f7f0ecad2f5721ee84470db9f415cdcb57f417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Sep 2025 14:46:39 +0200 Subject: [PATCH 38/38] Ignore `.osu` files not placed at top level of beatmap archive on import Closes https://github.com/ppy/osu/issues/34677. --- osu.Game/Beatmaps/BeatmapImporter.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapImporter.cs b/osu.Game/Beatmaps/BeatmapImporter.cs index 28997509dc..f80c4de4ea 100644 --- a/osu.Game/Beatmaps/BeatmapImporter.cs +++ b/osu.Game/Beatmaps/BeatmapImporter.cs @@ -367,7 +367,11 @@ namespace osu.Game.Beatmaps { var beatmaps = new List(); - foreach (var file in beatmapSet.Files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase))) + // stable appears to ignore `.osu` files which are not placed at the top level of the beatmap archive. + // the logic that achieves this is very difficult to make sense of, but appears to be located somewhere around + // https://github.com/peppy/osu-stable-reference/blob/67795dba3c308e7d0493b296149dcb073ca47ecb/osu!/GameplayElements/Beatmaps/BeatmapManager.cs#L207-L208 + // only testing the `/` path separator character is sufficient as `RealmNamedFileUsage`s are normalised to use the front slash unix path separator convention + foreach (var file in beatmapSet.Files.Where(f => !f.Filename.Contains('/') && f.Filename.EndsWith(@".osu", StringComparison.OrdinalIgnoreCase))) { using (var memoryStream = new MemoryStream(Files.Store.Get(file.File.GetStoragePath()))) // we need a memory stream so we can seek {