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

Fix SkinEditorOverlay freezing when ReplayPlayer screen exits early #26149

Merged
merged 4 commits into from
Feb 22, 2024

Conversation

rushiiMachine
Copy link
Contributor

@rushiiMachine rushiiMachine commented Dec 26, 2023

This addresses issue #25877

Originally when popping in, the ReplayPlayer was loaded first (if previous screen was MainMenu), and afterwards the SkinEditor component was loaded asynchronously. However, if the ReplayPlayer screen exits quickly (like in the event the beatmap has no objects), the skin editor component has not finished initializing (this is before it was even added to the component tree, so it's still not marked Visible), then the screen exiting will cause OsuGame to call SetTarget(newScreen) -> setTarget(...) which sees that the cached skinEditor is not visible yet, and hides/nulls the field. This is the point where LoadComponentAsync(editor, ...) finishes, and the callback sees that the cached skinEditor field is now different (null) than the one that was loaded, and never adds it to the component tree. This occurrence is unhandled and as such the SkinEditorOverlay never hides itself, consuming all input infinitely.

This PR changes the loading to start loading the ReplayPlayer after the SkinEditor has been loaded and added to the component tree. Additionally, this lowers the exit delay for ReplayPlayer and changes the "no hit objects" notification to not be an error since it's a controlled exit.

Originally when popping in, the ReplayPlayer was loaded first (if previous screen was MainMenu), and afterwards the SkinEditor component was loaded asynchronously. However, if the ReplayPlayer screen exits quickly (like in the event the beatmap has no objects), the skin editor component has not finished initializing (this is before it was even added to the component tree, so it's still not marked `Visible`), then the screen exiting will cause `OsuGame` to call SetTarget(newScreen) -> setTarget(...) which sees that the cached `skinEditor` is not visible yet, and hides/nulls the field. This is the point where LoadComponentAsync(editor, ...) finishes, and the callback sees that the cached skinEditor field is now different (null) than the one that was loaded, and never adds it to the component tree. This occurrence is unhandled and as such the SkinEditorOverlay never hides itself, consuming all input infinitely.

This PR changes the loading to start loading the ReplayPlayer *after* the SkinEditor has been loaded and added to the component tree.
Additionally, this lowers the exit delay for ReplayPlayer and changes the "no hit objects" notification to not be an error since it's a controlled exit.
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.

Test coverage would be useful (if feasible)

@@ -547,7 +547,7 @@ private IBeatmap loadPlayableBeatmap(Mod[] gameplayMods, CancellationToken cance

if (playable.HitObjects.Count == 0)
{
Logger.Log("Beatmap contains no hit objects!", level: LogLevel.Error);
Logger.Log("Beatmap contains no hit objects!", level: LogLevel.Important);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please change this back. This is an error condition.

Copy link
Contributor Author

@rushiiMachine rushiiMachine Dec 26, 2023

Choose a reason for hiding this comment

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

This is an error condition

Why so? It's a handled exit that can be caused by users intentionally (not like an uncaught exception).
I can spot another condition in Player that looks like it should actually be shown as an error

if (gameplayMods.Any(m => m is UnknownMod))
{
Logger.Log("Gameplay was started with an unknown mod applied.", level: LogLevel.Important);
return;

Copy link
Collaborator

Choose a reason for hiding this comment

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

I can spot another condition in Player that looks like it should actually be shown as an error

sure, it probably should be. point is that it's an unexpected condition and it should be communicated to the user as such. i believe stable also posts an error-ish notification on this same circumstance.

Copy link
Sponsor Member

Choose a reason for hiding this comment

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

The game also posts Important level, FWIW. One may argue that important is more correct here because it's not the game erroring but the beatmap just being wrong, dunno.

osu/osu.Game/OsuGame.cs

Lines 1179 to 1190 in cc800a1

if (entry.Level < LogLevel.Important || entry.Target > LoggingTarget.Database || entry.Target == null) return;
const int short_term_display_limit = 3;
if (recentLogCount < short_term_display_limit)
{
Schedule(() => Notifications.Post(new SimpleErrorNotification
{
Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb,
Text = entry.Message.Truncate(256) + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty),
}));
}

Copy link
Contributor Author

@rushiiMachine rushiiMachine Dec 27, 2023

Choose a reason for hiding this comment

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

One may argue that important is more correct here because it's not the game erroring but the beatmap just being wrong

Yeah that's what makes more sense to me. When I see the bomb icon I think that something has gone wrong, not just a controlled exit condition.
Seems very odd to report an issue not actually caused by the game itself as an error. Don't Error level logs also get reported to sentry?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Don't Error level logs also get reported to sentry?

If there is no associated exception (which there is not in this case) then it will not be.

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 stable also posts an error-ish notification on this same circumstance.

Stable handles this in an even worse way, just presenting an error of "beatmap could not be loaded"

@@ -316,7 +316,7 @@ protected override void LoadComplete()
base.LoadComplete();

if (!LoadedBeatmapSuccessfully)
Scheduler.AddDelayed(this.Exit, 3000);
Scheduler.AddDelayed(this.Exit, RESULTS_DISPLAY_DELAY);
Copy link
Collaborator

@bdach bdach Dec 26, 2023

Choose a reason for hiding this comment

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

Why reuse a results-related const? What does one have to do with the other?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also why was this shortened to begin with?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The delay was way too awkwardly long from a ux perspective, given it's a completely blank screen with only the background. For comparison, when trying to actually play a map with 0 objects, the delay before returning to song select isn't 3s like here either.

@bdach
Copy link
Collaborator

bdach commented Dec 26, 2023

and the callback sees that the cached skinEditor field is now different (null) than the one that was loaded, and never adds it to the component tree. This occurrence is unhandled and as such the SkinEditorOverlay never hides itself, consuming all input infinitely.

How does reordering the operations fix this?

It sounds to me that the skin editor should be doing something to hide itself in this case rather than remain in a zombie state consuming all input forevermore. The presence of player or whatever other screen seems purely incidental in this.

@rushiiMachine
Copy link
Contributor Author

rushiiMachine commented Dec 26, 2023

How does reordering the operations fix this?

Since the SkinEditor is added into the component tree and therefore marked Visible first, in the event the ReplayPlayer screen gets added and then exits afterwards does not cause setTarget() to nullify the skinEditor, since it's already marked Visisble
This whole loading scheme seems pretty jank and easily fragile though.

It sounds to me that the skin editor should be doing something to hide itself

Then it would exit back into the main menu, I don't think that would be correct behavior either.

Test coverage would be useful (if feasible)

Truthfully I have no idea how I would even test this, ideas would be welcome though

@bdach
Copy link
Collaborator

bdach commented Dec 26, 2023

What I'm getting from this exchange is that the real problem appears to be that SetTarget() expects the skin editor to be loaded. Which generally does not have to be true in any circumstance where that method is called.

So I'd say the real fix is to delay the call until the skin editor is ready to receive it. Probably by plugging in another condition into this check:

if (!target.IsLoaded)
{
Scheduler.AddOnce(setTarget, target);
return;
}

If the SkinEditor was created already but not finished initializing, wait for it to initialize before handling a screen change, which could possibly null the skin editor. Additionally, in the case the skin editor is already loaded but hidden, make sure to show gameplay.
@@ -316,7 +320,7 @@ protected override void LoadComplete()
base.LoadComplete();

if (!LoadedBeatmapSuccessfully)
Scheduler.AddDelayed(this.Exit, 3000);
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

what's going on here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@peppy
Copy link
Sponsor Member

peppy commented Jan 15, 2024

No test coverage? Seems worthwhile to have, and will help with review.

@bdach
Copy link
Collaborator

bdach commented Feb 22, 2024

As far as I can tell this fixes the (pretty bad issue) and adding a test is going to be annoying, so I'll just get this in on manual testing.

I'll also keep the irrelevant log severity / transition duration changes because they don't really matter.

@bdach bdach enabled auto-merge February 22, 2024 13:22
@bdach bdach merged commit 9018579 into ppy:master Feb 22, 2024
10 of 11 checks passed
@rushiiMachine rushiiMachine deleted the fix-skin-editor-init-fail branch February 22, 2024 22:24
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.

3 participants