Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite match subscreen to remove bindables #32669

Merged
merged 8 commits into from
Apr 8, 2025

Conversation

smoogipoo
Copy link
Contributor

@smoogipoo smoogipoo commented Apr 4, 2025

Fixes #32503
Split out from #32250, with minor changes.

This finalises the refactorings that have been going on for a while now.

In a similar fashion to #31882, this is a full rewrite of MultiplayerMatchSubScreen that no longer inherits (and finally removes) RoomSubScreen. The primary goal here is to remove the beatmap/ruleset/mod/"selected item" bindables and use MultiplayerRoom states.

Because it's a rewrite, I suggest going through the screen anew (rather than looking at the diff) and possibly comparing to the implementation of PlaylistsRoomSubScreen.

@smoogipoo smoogipoo changed the title Rewrite match subscreen to use full online state Rewrite match subscreen to remove bindables Apr 4, 2025
using osuTK;
using Container = osu.Framework.Graphics.Containers.Container;
using ParticipantsList = osu.Game.Screens.OnlinePlay.Multiplayer.Participants.ParticipantsList;

namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
[Cached]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order to address #32250 (comment), I've kept the subscreen being cached as-is, simplifying the settings overlay implementation to invoke ShowSongSelection().

Comment on lines +396 to +399
LoadComponent(userModsSelectOverlay = new MultiplayerUserModSelectOverlay
{
Beatmap = { BindTarget = Beatmap }
});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NB: In #32250, the beatmap wasn't bound which caused the footer to not update.

@smoogipoo smoogipoo force-pushed the refactor-match-subscreen branch from cf6d937 to 286b3d9 Compare April 4, 2025 01:49
Copy link
Collaborator

@bdach bdach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well this one didn't get much easier to review.

Spotted a few things from reading the code, once those are fixed I'll probably just do a test in-game and click buttons if I can't break it.

/// Responds to changes in the active room to adjust the visibility of the settings and main content.
/// Only the settings overlay is visible while the room isn't created, and only the main content is visible after creation.
/// </summary>
private void onRoomUpdated() => Scheduler.AddOnce(() =>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scheduler.AddOnce() does not debounce correctly if it's given an inline lambda like this because it's going to fail an equality check framework-side to determine if it should debounce or not. The two options are to either make the body of this a named method (this is the most idiomatic one we use), or to utilise the closure-eliminating overload of this, and have the inner lambda be a static (MultiplayerMatchSubScreen screen) => { } (don't recommend that one).

Copy link
Contributor Author

@smoogipoo smoogipoo Apr 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get it. Have you actually tested this? This is very idiomatic code in multiplayer, and it would also mean stuff like this is broken:

private void onUserJoined(MultiplayerRoomUser user)
=> Scheduler.AddOnce(() => userJoinedSample?.Play());
private void onUserLeft(MultiplayerRoomUser user)
=> Scheduler.AddOnce(() => userLeftSample?.Play());
private void onUserKicked(MultiplayerRoomUser user)
=> Scheduler.AddOnce(() => userKickedSample?.Play());
private void onHostChanged(MultiplayerRoomUser? host)
{
if (host != null)
Scheduler.AddOnce(() => hostChangedSample?.Play());
}

Which was added not by me, here: ef5e37c. So two people apparently believe this method works completely differently than it actually does. If this is the case I will do everything in my power to obliterate this method.

See also this o!f test: https://github.com/ppy/osu-framework/blob/5a7056a434be77ff84ecffddd27ac5617b400dac/osu.Framework.Tests/Threading/SchedulerTest.cs#L427-L439

Edit: I can't repro with:

diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
index d464362fda..cb84ae4706 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
@@ -416,6 +416,9 @@ protected override void LoadComplete()
             onRoomUpdated();
             updateGameplayState();
             updateUserActivity();
+
+            onRoomUpdated();
+            onRoomUpdated();
         }
 
         /// <summary>

Copy link
Collaborator

@bdach bdach Apr 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I have not tested this specific code with a debugger. I did not remember the test above existed. I wrote something like

        [Test]
        public void TestAddOnce()
        {
            var scheduler = new Scheduler();
            int invocations = 0;

            scheduler.AddOnce(() => invocations++);
            scheduler.AddOnce(() => invocations++);
            scheduler.Update();

            Assert.That(invocations, Is.EqualTo(1));
        }

and that fails. Which I guess isn't strictly the same what you have because you're never repeating that lambda again, but as that code comment in the test you linked says, I've at this point internalised that AddOnce() should not be called on inline lambdas because there be dragons and it might or it might not work as intended.

See ppy/osu-framework#6023 (comment) even, which is where that PR comment originates from, and #25076 (comment), which sparked it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the way it's conceptualised is that the lambda instance itself is always the same. i.e.

static object? a;

void m()
{
    a ??= new object();
}

static object? b;
static object? c;

void n()
{
    b ??= new object();
    c ??= new object();
}

The usage in multiplayer is always a case of the former.

Copy link
Collaborator

@bdach bdach Apr 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The usage in multiplayer is always a case of the former.

That's not even it actually. If you pull up the IL display in rider and put it to low level C# (for convenience) you'll see this:

    private void onRoomUpdated()
    {
      this.Scheduler.AddOnce(new Action((object) this, __methodptr(<onRoomUpdated>b__77_0)));
    }

This is allocating a delegate instance every time, but the compiler has figured out that it can make a method group delegate out of the body of the lambda, so the delegate equality check inside scheduler doesn't fail (because the lambdas are attached to a named method). This'll only work as long as the compiler does that and not something else.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you fine with it as is then? As I said this is pretty standard through all of multiplayer, feels weird if we're applying any changes only to this usage.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose we can keep it until a problem sure.

Copy link
Collaborator

@bdach bdach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empirically tested this some. Found only one thing, namely that clicking the countdown button on the bottom bar crashes with

[runtime] 2025-04-08 08:42:03 [verbose]: Too many unhandled exceptions, crashing out.
Unhandled exception. System.InvalidOperationException: Cannot show or hide a popover without a parent PopoverContainer in the hierarchy
   at osu.Framework.Extensions.PopoverExtensions.setTargetOnNearestPopover(Drawable origin, IHasPopover target)
   at osu.Framework.Extensions.PopoverExtensions.ShowPopover(IHasPopover hasPopover)
   at osu.Framework.Graphics.Containers.ClickableContainer.OnClick(ClickEvent e)
   at osu.Game.Graphics.UserInterface.OsuAnimatedButton.OnClick(ClickEvent e) in /Users/bdach/Documents/Work/open-source/osu/osu.Game/Graphics/UserInterface/OsuAnimatedButton.cs:line 117
   at osu.Framework.Input.ButtonEventManager`1.PropagateButtonEvent(IEnumerable`1 drawables, UIEvent e)
   at osu.Framework.Input.MouseButtonEventManager.handleClick(InputState state, List`1 targets)
   at osu.Framework.Input.MouseButtonEventManager.HandleButtonUp(InputState state, List`1 targets)

@smoogipoo
Copy link
Contributor Author

Good catch, didn't have any tests for that.

Beyond isolation, this stuff is also used to test (de-)serialisation.
Much-of-a-muchness in this case since it isn't too thoroughly tested,
but best do it anyway.
@bdach bdach merged commit b3626bf into ppy:master Apr 8, 2025
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Difficulty attributes in multiplayer and playlist extra mod select are not correctly shown
2 participants