Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ppy
GitHub Repository: ppy/osu
Path: blob/master/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs
5084 views
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Cursor;
using osu.Game.Input;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Menu;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Users;
using osu.Game.Utils;
using osuTK;
using Container = osu.Framework.Graphics.Containers.Container;

namespace osu.Game.Screens.OnlinePlay.Playlists
{
    public partial class PlaylistsRoomSubScreen : OnlinePlaySubScreen, IPreviewTrackOwner
    {
        /// <summary>
        /// Footer height.
        /// </summary>
        private const float footer_height = 50;

        /// <summary>
        /// Padding between content and footer.
        /// </summary>
        private const float footer_padding = 30;

        /// <summary>
        /// Internal padding of the content.
        /// </summary>
        private const float content_padding = 20;

        /// <summary>
        /// Padding between columns of the content.
        /// </summary>
        private const float column_padding = 10;

        /// <summary>
        /// Padding between rows of the content.
        /// </summary>
        private const float row_padding = 10;

        public override string Title { get; }

        public override string ShortTitle => "playlist";

        public override bool? ApplyModTrackAdjustments => true;

        public override bool DisallowExternalBeatmapRulesetChanges => true;

        /// <summary>
        /// Whether the user has confirmed they want to exit this screen in the presence of unsaved changes.
        /// </summary>
        protected bool ExitConfirmed { get; private set; }

        [Resolved]
        private IAPIProvider api { get; set; } = null!;

        [Resolved]
        private AudioManager audio { get; set; } = null!;

        [Resolved]
        private BeatmapManager beatmapManager { get; set; } = null!;

        [Resolved]
        private RulesetStore rulesets { get; set; } = null!;

        [Resolved]
        private PreviewTrackManager previewTrackManager { get; set; } = null!;

        [Resolved]
        private MusicController music { get; set; } = null!;

        [Resolved]
        private IdleTracker? idleTracker { get; set; }

        [Resolved]
        private OnlinePlayScreen? parentScreen { get; set; }

        [Resolved]
        private IOverlayManager? overlayManager { get; set; }

        [Resolved]
        private IDialogOverlay? dialogOverlay { get; set; }

        [Cached(typeof(OnlinePlayBeatmapAvailabilityTracker))]
        private readonly PlaylistsBeatmapAvailabilityTracker beatmapAvailabilityTracker;

        protected readonly Bindable<PlaylistItem?> SelectedItem = new Bindable<PlaylistItem?>();
        protected readonly Bindable<BeatmapInfo?> UserBeatmap = new Bindable<BeatmapInfo?>();
        protected readonly Bindable<RulesetInfo?> UserRuleset = new Bindable<RulesetInfo?>();
        protected readonly Bindable<IReadOnlyList<Mod>> UserMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());

        private readonly IBindable<bool> isIdle = new BindableBool();
        private readonly Room room;

        private Drawable roomContent = null!;
        private PlaylistsRoomUpdater roomUpdater = null!;
        private PlaylistsRoomSettingsOverlay settingsOverlay = null!;

        private MatchLeaderboard leaderboard = null!;
        private FillFlowContainer progressSection = null!;
        private DrawableRoomPlaylist drawablePlaylist = null!;

        private FillFlowContainer userModsSection = null!;
        private RoomModSelectOverlay userModsSelectOverlay = null!;

        private FillFlowContainer userStyleSection = null!;
        private Container<DrawableRoomPlaylistItem> userStyleDisplayContainer = null!;

        private Sample? sampleStart;
        private IDisposable? userModsSelectOverlayRegistration;

        public PlaylistsRoomSubScreen(Room room)
        {
            this.room = room;

            Title = room.RoomID == null ? "New playlist" : room.Name;
            Activity.Value = new UserActivity.InLobby(room);

            Padding = new MarginPadding { Top = Header.HEIGHT };

            beatmapAvailabilityTracker = new PlaylistsBeatmapAvailabilityTracker
            {
                PlaylistItem = { BindTarget = SelectedItem }
            };
        }

        [BackgroundDependencyLoader]
        private void load()
        {
            if (idleTracker != null)
                isIdle.BindTo(idleTracker.IsIdle);

            sampleStart = audio.Samples.Get(@"SongSelect/confirm-selection");

            InternalChild = new OsuContextMenuContainer
            {
                RelativeSizeAxes = Axes.Both,
                Child = new PopoverContainer
                {
                    RelativeSizeAxes = Axes.Both,
                    Children = new Drawable[]
                    {
                        roomUpdater = new PlaylistsRoomUpdater(room),
                        beatmapAvailabilityTracker,
                        new MultiplayerRoomSounds(),
                        new Container
                        {
                            RelativeSizeAxes = Axes.Both,
                            Padding = new MarginPadding
                            {
                                Horizontal = WaveOverlayContainer.WIDTH_PADDING,
                                Bottom = footer_height + footer_padding
                            },
                            Children = new[]
                            {
                                roomContent = new GridContainer
                                {
                                    RelativeSizeAxes = Axes.Both,
                                    RowDimensions = new[]
                                    {
                                        new Dimension(GridSizeMode.AutoSize),
                                        new Dimension(GridSizeMode.Absolute, row_padding),
                                    },
                                    Content = new[]
                                    {
                                        new Drawable[]
                                        {
                                            new PlaylistsRoomPanel(room)
                                            {
                                                SelectedItem = SelectedItem
                                            }
                                        },
                                        null,
                                        new Drawable[]
                                        {
                                            new Container
                                            {
                                                RelativeSizeAxes = Axes.Both,
                                                Masking = true,
                                                CornerRadius = 10,
                                                Children = new Drawable[]
                                                {
                                                    new Box
                                                    {
                                                        RelativeSizeAxes = Axes.Both,
                                                        Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary.
                                                    },
                                                    new GridContainer
                                                    {
                                                        RelativeSizeAxes = Axes.Both,
                                                        Padding = new MarginPadding(content_padding),
                                                        ColumnDimensions = new[]
                                                        {
                                                            new Dimension(),
                                                            new Dimension(GridSizeMode.Absolute, column_padding),
                                                            new Dimension(),
                                                            new Dimension(GridSizeMode.Absolute, column_padding),
                                                            new Dimension(),
                                                        },
                                                        Content = new[]
                                                        {
                                                            new Drawable?[]
                                                            {
                                                                new GridContainer
                                                                {
                                                                    RelativeSizeAxes = Axes.Both,
                                                                    RowDimensions = new[]
                                                                    {
                                                                        new Dimension(GridSizeMode.AutoSize),
                                                                        new Dimension(),
                                                                        new Dimension(GridSizeMode.AutoSize),
                                                                    },
                                                                    Content = new[]
                                                                    {
                                                                        new Drawable[]
                                                                        {
                                                                            new OverlinedPlaylistHeader(room),
                                                                        },
                                                                        new Drawable[]
                                                                        {
                                                                            drawablePlaylist = new DrawableRoomPlaylist
                                                                            {
                                                                                RelativeSizeAxes = Axes.Both,
                                                                                SelectedItem = { BindTarget = SelectedItem },
                                                                                AllowSelection = true,
                                                                                AllowShowingResults = true,
                                                                                RequestResults = showResults
                                                                            }
                                                                        },
                                                                        new Drawable[]
                                                                        {
                                                                            new AddPlaylistToCollectionButton(room)
                                                                            {
                                                                                Margin = new MarginPadding { Top = 5 },
                                                                                RelativeSizeAxes = Axes.X,
                                                                                Size = new Vector2(1, 40)
                                                                            }
                                                                        }
                                                                    }
                                                                },
                                                                null,
                                                                new GridContainer
                                                                {
                                                                    RelativeSizeAxes = Axes.Both,
                                                                    RowDimensions = new[]
                                                                    {
                                                                        new Dimension(GridSizeMode.AutoSize),
                                                                        new Dimension(GridSizeMode.AutoSize),
                                                                        new Dimension(GridSizeMode.AutoSize),
                                                                        new Dimension(GridSizeMode.AutoSize),
                                                                    },
                                                                    Content = new[]
                                                                    {
                                                                        new Drawable[]
                                                                        {
                                                                            userModsSection = new FillFlowContainer
                                                                            {
                                                                                RelativeSizeAxes = Axes.X,
                                                                                AutoSizeAxes = Axes.Y,
                                                                                Margin = new MarginPadding { Bottom = row_padding },
                                                                                Alpha = 0,
                                                                                Children = new Drawable[]
                                                                                {
                                                                                    new OverlinedHeader("Extra mods"),
                                                                                    new FillFlowContainer
                                                                                    {
                                                                                        AutoSizeAxes = Axes.Both,
                                                                                        Direction = FillDirection.Horizontal,
                                                                                        Spacing = new Vector2(10, 0),
                                                                                        Children = new Drawable[]
                                                                                        {
                                                                                            new UserModSelectButton
                                                                                            {
                                                                                                Anchor = Anchor.CentreLeft,
                                                                                                Origin = Anchor.CentreLeft,
                                                                                                Width = 90,
                                                                                                Height = 30,
                                                                                                Text = "Select",
                                                                                                Action = showUserModSelect,
                                                                                            },
                                                                                            new ModDisplay
                                                                                            {
                                                                                                Anchor = Anchor.CentreLeft,
                                                                                                Origin = Anchor.CentreLeft,
                                                                                                Current = UserMods,
                                                                                                Scale = new Vector2(0.8f),
                                                                                            }
                                                                                        }
                                                                                    }
                                                                                }
                                                                            }
                                                                        },
                                                                        new Drawable[]
                                                                        {
                                                                            userStyleSection = new FillFlowContainer
                                                                            {
                                                                                RelativeSizeAxes = Axes.X,
                                                                                AutoSizeAxes = Axes.Y,
                                                                                Margin = new MarginPadding { Bottom = row_padding },
                                                                                Alpha = 0,
                                                                                Children = new Drawable[]
                                                                                {
                                                                                    new OverlinedHeader("Difficulty"),
                                                                                    userStyleDisplayContainer = new Container<DrawableRoomPlaylistItem>
                                                                                    {
                                                                                        RelativeSizeAxes = Axes.X,
                                                                                        AutoSizeAxes = Axes.Y
                                                                                    }
                                                                                }
                                                                            }
                                                                        },
                                                                        new Drawable[]
                                                                        {
                                                                            progressSection = new FillFlowContainer
                                                                            {
                                                                                RelativeSizeAxes = Axes.X,
                                                                                AutoSizeAxes = Axes.Y,
                                                                                Margin = new MarginPadding { Bottom = row_padding },
                                                                                Alpha = 0,
                                                                                Direction = FillDirection.Vertical,
                                                                                Children = new Drawable[]
                                                                                {
                                                                                    new OverlinedHeader("Progress"),
                                                                                    new RoomLocalUserInfo(room),
                                                                                }
                                                                            }
                                                                        },
                                                                        new Drawable[]
                                                                        {
                                                                            new OverlinedHeader("Leaderboard")
                                                                        },
                                                                        new Drawable[]
                                                                        {
                                                                            leaderboard = new MatchLeaderboard(room)
                                                                            {
                                                                                RelativeSizeAxes = Axes.Both
                                                                            },
                                                                        }
                                                                    }
                                                                },
                                                                null,
                                                                new GridContainer
                                                                {
                                                                    RelativeSizeAxes = Axes.Both,
                                                                    RowDimensions = new[]
                                                                    {
                                                                        new Dimension(GridSizeMode.AutoSize)
                                                                    },
                                                                    Content = new[]
                                                                    {
                                                                        new Drawable[]
                                                                        {
                                                                            new OverlinedHeader("Chat")
                                                                        },
                                                                        new Drawable[]
                                                                        {
                                                                            new MatchChatDisplay(room)
                                                                            {
                                                                                RelativeSizeAxes = Axes.Both
                                                                            }
                                                                        }
                                                                    }
                                                                }
                                                            }
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                },
                                settingsOverlay = new PlaylistsRoomSettingsOverlay(room)
                                {
                                    EditPlaylist = () =>
                                    {
                                        if (this.IsCurrentScreen())
                                            this.Push(new PlaylistsSongSelectV2(room));
                                    }
                                }
                            }
                        },
                        new Container
                        {
                            Anchor = Anchor.BottomLeft,
                            Origin = Anchor.BottomLeft,
                            RelativeSizeAxes = Axes.X,
                            Height = footer_height,
                            Children = new Drawable[]
                            {
                                new Box
                                {
                                    RelativeSizeAxes = Axes.Both,
                                    Colour = Color4Extensions.FromHex(@"28242d") // Temporary.
                                },
                                new Container
                                {
                                    RelativeSizeAxes = Axes.Both,
                                    Padding = new MarginPadding(5),
                                    Child = new PlaylistsRoomFooter(room)
                                    {
                                        OnStart = startPlay,
                                        OnClose = closePlaylist
                                    }
                                }
                            }
                        }
                    }
                }
            };

            LoadComponent(userModsSelectOverlay = new RoomModSelectOverlay
            {
                SelectedItem = { BindTarget = SelectedItem },
                SelectedMods = { BindTarget = UserMods },
                Beatmap = { BindTarget = Beatmap },
                IsValidMod = _ => false
            });
        }

        protected override void LoadComplete()
        {
            base.LoadComplete();

            userModsSelectOverlayRegistration = overlayManager?.RegisterBlockingOverlay(userModsSelectOverlay);

            room.PropertyChanged += onRoomPropertyChanged;

            isIdle.BindValueChanged(_ => updatePollingRate(), true);
            beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateGameplayState());

            SelectedItem.BindValueChanged(onSelectedItemChanged);
            UserBeatmap.BindValueChanged(_ => updateGameplayState());
            UserMods.BindValueChanged(_ => updateGameplayState());
            UserRuleset.BindValueChanged(_ =>
            {
                // The user mod selection overlay is separate from the beatmap/ruleset style selection screen,
                // and so the validity of mods has to be confirmed separately after the ruleset is changed.
                validateUserMods();
                updateGameplayState();
            });

            updateSetupState();
            updateUserScore();
            updateGameplayState();
        }

        /// <summary>
        /// Responds to changes of the <see cref="Room"/>'s properties.
        /// </summary>
        /// <param name="sender">The <see cref="Room"/> that changed.</param>
        /// <param name="e">Describes the property that changed.</param>
        private void onRoomPropertyChanged(object? sender, PropertyChangedEventArgs e)
        {
            switch (e.PropertyName)
            {
                case nameof(Room.RoomID):
                    updateSetupState();
                    break;

                case nameof(Room.UserScore):
                    updateUserScore();
                    break;
            }
        }

        /// <summary>
        /// Responds to changes in <see cref="Room.RoomID"/> 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 updateSetupState()
        {
            if (room.RoomID == null)
            {
                // A new room is being created.
                // The main content should be hidden until the settings overlay is hidden, signaling the room is ready to be displayed.
                roomContent.Hide();
                settingsOverlay.Show();
            }
            else
            {
                roomContent.Show();
                settingsOverlay.Hide();

                // Scheduled because room properties are updated in arbitrary order.
                Schedule(() =>
                {
                    progressSection.Alpha = room.MaxAttempts != null ? 1 : 0;
                    drawablePlaylist.Items.ReplaceRange(0, drawablePlaylist.Items.Count, room.Playlist);

                    updateUserScore();

                    // Select an initial item for the user to help them get into a playable state quicker.
                    SelectedItem.Value = room.Playlist.FirstOrDefault();
                });
            }
        }

        /// <summary>
        /// Responds to changes in <see cref="Room.UserScore"/> to mark playlist items as completed.
        /// </summary>
        private void updateUserScore()
        {
            if (room.UserScore == null)
                return;

            if (drawablePlaylist.Items.Count == 0)
                return;

            foreach (var item in room.UserScore.PlaylistItemAttempts)
            {
                if (item.Passed)
                    drawablePlaylist.Items.Single(i => i.ID == item.PlaylistItemID).MarkCompleted();
            }
        }

        /// <summary>
        /// Adjusts the rate at which the <see cref="Room"/> is updated.
        /// </summary>
        private void updatePollingRate()
        {
            roomUpdater.TimeBetweenPolls.Value = isIdle.Value ? 30000 : 5000;
            Logger.Log($"Polling adjusted (selection: {roomUpdater.TimeBetweenPolls.Value})");
        }

        /// <summary>
        /// Responds to changes in <see cref="SelectedItem"/> to validate the user style and update the global gameplay state.
        /// </summary>
        private void onSelectedItemChanged(ValueChangedEvent<PlaylistItem?> item)
        {
            if (item.NewValue == null)
                return;

            // Always resetting the user beatmap style when a new item is selected is most intuitive.
            UserBeatmap.Value = null;

            if (item.NewValue.Freestyle)
            {
                // If freestyle is active, attempt to preserve the user ruleset style but only if the online item is from the osu! ruleset
                // (i.e. the beatmap is generally always convertible to the current ruleset, excluding custom rulesets).
                if (item.NewValue.RulesetID > 0)
                    UserRuleset.Value = null;
            }
            else
                UserRuleset.Value = null;

            validateUserMods();
            updateGameplayState();
        }

        /// <summary>
        /// Validates the user mod style against the selected item and ruleset style.
        /// </summary>
        private void validateUserMods()
        {
            if (SelectedItem.Value == null)
                return;

            PlaylistItem item = SelectedItem.Value;
            RulesetInfo gameplayRuleset = UserRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!;
            Mod[] allowedMods = ModUtils.EnumerateUserSelectableFreeMods(MatchType.Playlists, item.RequiredMods, item.AllowedMods, item.Freestyle, gameplayRuleset.CreateInstance());

            UserMods.Value = UserMods.Value.Where(m => allowedMods.Any(a => m.GetType() == a.GetType())).ToArray();
        }

        /// <summary>
        /// Updates the global states in preparation for a new gameplay session.
        /// </summary>
        private void updateGameplayState()
        {
            if (!this.IsCurrentScreen() || SelectedItem.Value == null)
                return;

            PlaylistItem item = SelectedItem.Value;

            IBeatmapInfo gameplayBeatmap = UserBeatmap.Value ?? item.Beatmap;
            RulesetInfo gameplayRuleset = UserRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!;
            Ruleset rulesetInstance = gameplayRuleset.CreateInstance();
            Mod[] allowedMods = ModUtils.EnumerateUserSelectableFreeMods(MatchType.Playlists, item.RequiredMods, item.AllowedMods, item.Freestyle, gameplayRuleset.CreateInstance());

            // Update global gameplay state to correspond to the new selection.
            // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info
            var localBeatmap = beatmapManager.QueryOnlineBeatmapId(gameplayBeatmap.OnlineID);
            Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
            Ruleset.Value = gameplayRuleset;
            Mods.Value = UserMods.Value.Concat(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))).ToArray();

            // Update UI elements to reflect the new selection.
            bool freemods = item.Freestyle || allowedMods.Length > 0;
            bool freestyle = item.Freestyle;

            if (freemods)
            {
                userModsSection.Show();
                userModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType());
            }
            else
            {
                userModsSection.Hide();
                userModsSelectOverlay.Hide();
                userModsSelectOverlay.IsValidMod = _ => false;
            }

            if (freestyle)
            {
                userStyleSection.Show();

                PlaylistItem gameplayItem = item.With(ruleset: gameplayRuleset.OnlineID, beatmap: new Optional<IBeatmapInfo>(gameplayBeatmap));
                DrawableRoomPlaylistItem? currentDisplay = userStyleDisplayContainer.SingleOrDefault();

                if (!gameplayItem.Equals(currentDisplay?.Item))
                {
                    userStyleDisplayContainer.Child = currentDisplay = new DrawableRoomPlaylistItem(gameplayItem, true)
                    {
                        AllowReordering = false,
                        RequestEdit = _ => showUserStyleSelect()
                    };
                }

                currentDisplay.AllowEditing = localBeatmap != null;
            }
            else
                userStyleSection.Hide();
        }

        /// <summary>
        /// Pushes a <see cref="Player"/> to start gameplay with the current selection.
        /// </summary>
        private void startPlay()
        {
            if (!this.IsCurrentScreen() || SelectedItem.Value == null)
                return;

            PlaylistItem item = SelectedItem.Value;

            // Required for validation inside the player.
            RulesetInfo gameplayRuleset = UserRuleset.Value ?? rulesets.GetRuleset(item.RulesetID)!;
            IBeatmapInfo gameplayBeatmap = UserBeatmap.Value ?? item.Beatmap;
            PlaylistItem gameplayItem = item.With(ruleset: gameplayRuleset.OnlineID, beatmap: new Optional<IBeatmapInfo>(gameplayBeatmap));

            sampleStart?.Play();

            // fallback is to allow this class to operate when there is no parent OnlineScreen (testing purposes).
            var targetScreen = (Screen?)parentScreen ?? this;
            targetScreen.Push(new PlayerLoader(() => new PlaylistsPlayer(room, gameplayItem)
            {
                Exited = () => leaderboard.RefetchScores()
            }));
        }

        /// <summary>
        /// Shows the user mod selection.
        /// </summary>
        private void showUserModSelect()
        {
            if (!this.IsCurrentScreen() || SelectedItem.Value == null)
                return;

            userModsSelectOverlay.Show();
        }

        /// <summary>
        /// Shows the user style selection.
        /// </summary>
        private void showUserStyleSelect()
        {
            if (!this.IsCurrentScreen() || SelectedItem.Value == null)
                return;

            this.Push(new PlaylistsRoomFreestyleSelect(room, SelectedItem.Value)
            {
                Beatmap = { BindTarget = UserBeatmap },
                Ruleset = { BindTarget = UserRuleset }
            });
        }

        /// <summary>
        /// Shows the results screen for a playlist item.
        /// </summary>
        private void showResults(PlaylistItem item)
        {
            if (!this.IsCurrentScreen())
                return;

            Debug.Assert(room.RoomID != null);

            // fallback is to allow this class to operate when there is no parent OnlineScreen (testing purposes).
            var targetScreen = (Screen?)parentScreen ?? this;
            targetScreen.Push(new PlaylistItemUserBestResultsScreen(room.RoomID.Value, item, api.LocalUser.Value.OnlineID));
        }

        /// <summary>
        /// May be invoked by the owner of the room to permanently close the room ahead of its intended end date.
        /// </summary>
        private void closePlaylist()
        {
            dialogOverlay?.Push(new ClosePlaylistDialog(room, () =>
            {
                var request = new ClosePlaylistRequest(room.RoomID!.Value);
                request.Success += () => room.EndDate = DateTimeOffset.UtcNow;
                api.Queue(request);
            }));
        }

        public override void OnEntering(ScreenTransitionEvent e)
        {
            base.OnEntering(e);
            beginHandlingTrack();
        }

        public override void OnSuspending(ScreenTransitionEvent e)
        {
            onLeaving();
            base.OnSuspending(e);
        }

        public override void OnResuming(ScreenTransitionEvent e)
        {
            base.OnResuming(e);
            beginHandlingTrack();

            // Required to update beatmap/ruleset when resuming from style selection.
            updateGameplayState();
        }

        public override bool OnExiting(ScreenExitEvent e)
        {
            if (!ensureExitConfirmed())
                return true;

            if (room.RoomID != null)
                api.Queue(new PartRoomRequest(room));

            onLeaving();
            return base.OnExiting(e);
        }

        public override bool OnBackButton()
        {
            if (room.RoomID == null)
            {
                if (!ensureExitConfirmed())
                    return true;

                settingsOverlay.Hide();
                return base.OnBackButton();
            }

            if (userModsSelectOverlay.State.Value == Visibility.Visible)
            {
                userModsSelectOverlay.Hide();
                return true;
            }

            if (settingsOverlay.State.Value == Visibility.Visible)
            {
                settingsOverlay.Hide();
                return true;
            }

            return base.OnBackButton();
        }

        private void onLeaving()
        {
            // Must hide this overlay because it is added to a global container.
            userModsSelectOverlay.Hide();

            endHandlingTrack();
        }

        /// <summary>
        /// Handles changes in the track to keep it looping while active.
        /// </summary>
        private void beginHandlingTrack()
        {
            Beatmap.BindValueChanged(applyLoopingToTrack, true);
        }

        /// <summary>
        /// Stops looping the current track and stops handling further changes to the track.
        /// </summary>
        private void endHandlingTrack()
        {
            Beatmap.ValueChanged -= applyLoopingToTrack;
            Beatmap.Value.Track.Looping = false;

            previewTrackManager.StopAnyPlaying(this);
        }

        /// <summary>
        /// Invoked on changes to the beatmap to loop the track. See: <see cref="beginHandlingTrack"/>.
        /// </summary>
        /// <param name="beatmap">The beatmap change event.</param>
        private void applyLoopingToTrack(ValueChangedEvent<WorkingBeatmap> beatmap)
        {
            if (!this.IsCurrentScreen())
                return;

            beatmap.NewValue.PrepareTrackForPreview(true);
            music.EnsurePlayingSomething();
        }

        /// <summary>
        /// Prompts the user to discard unsaved changes to the room before exiting.
        /// </summary>
        /// <returns><c>true</c> if the user has confirmed they want to exit.</returns>
        private bool ensureExitConfirmed()
        {
            if (ExitConfirmed)
                return true;

            if (api.State.Value != APIState.Online)
                return true;

            bool hasUnsavedChanges = room.RoomID == null && room.Playlist.Count > 0;

            if (dialogOverlay == null || !hasUnsavedChanges)
                return true;

            // if the dialog is already displayed, block exiting until the user explicitly makes a decision.
            if (dialogOverlay.CurrentDialog is ConfirmDiscardChangesDialog discardChangesDialog)
            {
                discardChangesDialog.Flash();
                return false;
            }

            dialogOverlay.Push(new ConfirmDiscardChangesDialog(() =>
            {
                ExitConfirmed = true;
                settingsOverlay.Hide();
                this.Exit();
            }));

            return false;
        }

        // Block all input to this screen during gameplay/etc when the parent screen is no longer current.
        // Normally this would be handled by ScreenStack, but we are in a child ScreenStack.
        public override bool PropagatePositionalInputSubTree => base.PropagatePositionalInputSubTree && (parentScreen?.IsCurrentScreen() ?? this.IsCurrentScreen());

        // Block all input to this screen during gameplay/etc when the parent screen is no longer current.
        // Normally this would be handled by ScreenStack, but we are in a child ScreenStack.
        public override bool PropagateNonPositionalInputSubTree => base.PropagateNonPositionalInputSubTree && (parentScreen?.IsCurrentScreen() ?? this.IsCurrentScreen());

        protected override BackgroundScreen CreateBackground() => new RoomBackgroundScreen(room.Playlist.FirstOrDefault())
        {
            SelectedItem = { BindTarget = SelectedItem }
        };

        protected override void Dispose(bool isDisposing)
        {
            base.Dispose(isDisposing);

            userModsSelectOverlayRegistration?.Dispose();
            room.PropertyChanged -= onRoomPropertyChanged;
        }
    }
}