1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-18 16:10:48 +08:00
Files
osu-lazer/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs
T

972 lines
44 KiB
C#

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
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.Extensions.ObjectExtensions;
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.Online;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets;
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.Match;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist;
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Users;
using osu.Game.Utils;
using osuTK;
using ParticipantsList = osu.Game.Screens.OnlinePlay.Multiplayer.Participants.ParticipantsList;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
[Cached]
public partial class MultiplayerMatchSubScreen : OnlinePlaySubScreen, IPreviewTrackOwner, IHandlePresentBeatmap
{
/// <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 => "room";
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; }
/// <summary>
/// Used for testing - whether the local user style can be edited.
/// False if the beatmap hasn't been downloaded yet, or if freestyle isn't enabled.
/// </summary>
internal bool UserStyleEditingEnabled
{
get
{
if (!userStyleDisplayContainer.IsPresent)
return false;
return userStyleDisplayContainer.SingleOrDefault()?.AllowEditing == true;
}
}
[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 OnlinePlayScreen? parentScreen { get; set; }
[Resolved]
private IOverlayManager? overlayManager { get; set; }
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
[Resolved]
private MultiplayerClient client { get; set; } = null!;
[Resolved]
private OsuGame? game { get; set; }
[Cached(typeof(OnlinePlayBeatmapAvailabilityTracker))]
private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new MultiplayerBeatmapAvailabilityTracker();
private readonly Room room;
private Drawable roomContent = null!;
private MultiplayerMatchSettingsOverlay settingsOverlay = null!;
private FillFlowContainer userModsSection = null!;
private MultiplayerUserModSelectOverlay userModsSelectOverlay = null!;
private FillFlowContainer userStyleSection = null!;
private Container<DrawableRoomPlaylistItem> userStyleDisplayContainer = null!;
private Sample? sampleStart;
private IDisposable? userModsSelectOverlayRegistration;
private long lastPlaylistItemId;
private bool isRoomJoined;
public MultiplayerMatchSubScreen(Room room)
{
this.room = room;
Title = room.RoomID == null ? "New room" : room.Name;
Activity.Value = new UserActivity.InLobby(room);
Padding = new MarginPadding { Top = Header.HEIGHT };
}
[BackgroundDependencyLoader]
private void load()
{
sampleStart = audio.Samples.Get(@"SongSelect/confirm-selection");
InternalChild = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
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 MultiplayerRoomPanel(room)
{
OnEdit = () => settingsOverlay.Show()
}
},
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)
},
Content = new[]
{
new Drawable[]
{
new ParticipantsListHeader()
},
new Drawable[]
{
new ParticipantsList
{
RelativeSizeAxes = Axes.Both
},
}
}
},
null,
new GridContainer
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, 5),
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[]
{
new OverlinedHeader("Beatmap queue")
},
new Drawable[]
{
new AddItemButton
{
RelativeSizeAxes = Axes.X,
Height = 30,
Text = "Add item",
Action = () => ShowSongSelect()
},
},
null,
new Drawable[]
{
new MultiplayerPlaylist
{
RelativeSizeAxes = Axes.Both,
RequestEdit = ShowSongSelect,
RequestResults = showResults
}
},
new Drawable[]
{
userModsSection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Top = 10 },
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 MultiplayerUserModDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Scale = new Vector2(0.8f),
},
}
},
}
}
},
new Drawable[]
{
userStyleSection = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Top = 10 },
Alpha = 0,
Children = new Drawable[]
{
new OverlinedHeader("Difficulty"),
userStyleDisplayContainer = new Container<DrawableRoomPlaylistItem>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}
}
},
},
},
},
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 MultiplayerMatchSettingsOverlay(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 MultiplayerMatchFooter()
}
}
}
}
}
};
LoadComponent(userModsSelectOverlay = new MultiplayerUserModSelectOverlay
{
Beatmap = { BindTarget = Beatmap }
});
}
protected override void LoadComplete()
{
base.LoadComplete();
userModsSelectOverlayRegistration = overlayManager?.RegisterBlockingOverlay(userModsSelectOverlay);
client.RoomUpdated += onRoomUpdated;
client.SettingsChanged += onSettingsChanged;
client.ItemChanged += onItemChanged;
client.UserStyleChanged += onUserStyleChanged;
client.UserModsChanged += onUserModsChanged;
client.LoadRequested += onLoadRequested;
beatmapAvailabilityTracker.Availability.BindValueChanged(onBeatmapAvailabilityChanged, true);
onRoomUpdated();
updateGameplayState();
updateUserActivity();
}
/// <summary>
/// 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(() =>
{
bool wasRoomJoined = isRoomJoined;
isRoomJoined = client.Room != null;
// Creating a room.
if (!wasRoomJoined && !isRoomJoined)
{
roomContent.Hide();
settingsOverlay.Show();
}
// Joining a room.
if (!wasRoomJoined && isRoomJoined)
{
roomContent.Show();
settingsOverlay.Hide();
}
// Leaving a room.
if (wasRoomJoined && !isRoomJoined)
{
Logger.Log($"{this} exiting due to loss of room or connection");
if (this.IsCurrentScreen())
this.Exit();
else
ValidForResume = false;
}
});
/// <summary>
/// Responds to changes in the room's settings to update the gameplay state and local user's activity.
/// </summary>
private void onSettingsChanged(MultiplayerRoomSettings settings)
{
if (settings.PlaylistItemId != lastPlaylistItemId)
{
onActivePlaylistItemChanged();
lastPlaylistItemId = settings.PlaylistItemId;
}
updateUserActivity();
}
/// <summary>
/// Responds to changes in the active playlist item to update the gameplay state.
/// </summary>
private void onItemChanged(MultiplayerPlaylistItem item)
{
if (item.ID == client.Room?.Settings.PlaylistItemId)
onActivePlaylistItemChanged();
}
/// <summary>
/// Responds to changes in the active playlist item resulting from the playlist item being edited or the room settings changing.
/// </summary>
private void onActivePlaylistItemChanged()
{
if (client.Room == null)
return;
// Check if we need to make this the current screen as a result of the beatmap set changing while the user's selecting a style.
if (this.GetChildScreen() is MultiplayerMatchFreestyleSelect)
{
MultiplayerPlaylistItem item = client.Room.CurrentPlaylistItem;
var newBeatmap = beatmapManager.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", item.BeatmapID);
if (!Beatmap.Value.BeatmapSetInfo.Equals(newBeatmap?.BeatmapSet))
this.MakeCurrent();
}
Scheduler.AddOnce(updateGameplayState);
}
/// <summary>
/// Responds to changes in the local user's style to update the gameplay state.
/// </summary>
private void onUserStyleChanged(MultiplayerRoomUser user)
{
if (user.Equals(client.LocalUser))
Scheduler.AddOnce(updateGameplayState);
}
/// <summary>
/// Responds to changes in the local user's mods style to update the gameplay state.
/// </summary>
private void onUserModsChanged(MultiplayerRoomUser user)
{
if (user.Equals(client.LocalUser))
Scheduler.AddOnce(updateGameplayState);
}
/// <summary>
/// Responds to notifications from the server that a gameplay session is ready to attempt to start the gameplay session.
/// </summary>
private void onLoadRequested()
{
if (client.Room == null || client.LocalUser == null)
return;
// In the case of spectating, IMultiplayerClient.LoadRequested can be fired while the game is still spectating a previous session.
// For now, we want to game to switch to the new game so need to request exiting from the play screen.
if (!parentScreen.IsCurrentScreen())
{
parentScreen.MakeCurrent();
Schedule(onLoadRequested);
return;
}
if (!this.IsCurrentScreen())
{
this.MakeCurrent();
Schedule(onLoadRequested);
return;
}
// Ensure all the gameplay states are up-to-date, forgoing any misordering/scheduling shenanigans.
updateGameplayState();
// ... And then check that the set gameplay state is valid.
// When spectating, we'll receive LoadRequested() from the server even though we may not yet have the beatmap.
// In that case, this method will be invoked again after availability changes in onBeatmapAvailabilityChanged().
if (Beatmap.IsDefault)
{
Logger.Log("Aborting gameplay start - beatmap not downloaded.");
return;
}
// Start the gameplay session.
sampleStart?.Play();
int[] userIds = client.CurrentMatchPlayingUserIds.ToArray();
MultiplayerRoomUser[] users = userIds.Select(id => client.Room.Users.First(u => u.UserID == id)).ToArray();
// fallback is to allow this class to operate when there is no parent OnlineScreen (testing purposes).
var targetScreen = (Screen?)parentScreen ?? this;
switch (client.LocalUser.State)
{
case MultiplayerUserState.Spectating:
targetScreen.Push(new MultiSpectatorScreen(room, users.Take(PlayerGrid.MAX_PLAYERS).ToArray()));
break;
default:
targetScreen.Push(new MultiplayerPlayerLoader(() => new MultiplayerPlayer(room, new PlaylistItem(client.Room.CurrentPlaylistItem), users)));
break;
}
}
/// <summary>
/// Responds to changes in the local user's beatmap availability to notify the server and prepare the gameplay session.
/// </summary>
private void onBeatmapAvailabilityChanged(ValueChangedEvent<BeatmapAvailability> e)
{
if (client.Room == null || client.LocalUser == null)
return;
client.ChangeBeatmapAvailability(e.NewValue).FireAndForget();
switch (e.NewValue.State)
{
case DownloadState.LocallyAvailable:
updateGameplayState();
// Optimistically enter spectator if the match is in progress while spectating.
if (client.LocalUser.State == MultiplayerUserState.Spectating && (client.Room.State == MultiplayerRoomState.WaitingForLoad || client.Room.State == MultiplayerRoomState.Playing))
onLoadRequested();
break;
case DownloadState.NotDownloaded:
updateGameplayState();
if (client.LocalUser.State == MultiplayerUserState.Ready)
client.ChangeState(MultiplayerUserState.Idle);
break;
}
}
/// <summary>
/// Updates the local user's activity to publish their presence in the room.
/// </summary>
private void updateUserActivity()
{
if (client.Room == null)
return;
if (Activity.Value is not UserActivity.InLobby existing || existing.RoomName != client.Room.Settings.Name)
Activity.Value = new UserActivity.InLobby(client.Room);
}
/// <summary>
/// Updates the global beatmap/ruleset/mods in preparation for a new gameplay session.
/// </summary>
private void updateGameplayState()
{
if (client.Room == null || client.LocalUser == null)
return;
MultiplayerPlaylistItem item = client.Room.CurrentPlaylistItem;
int gameplayBeatmapId = client.LocalUser.BeatmapId ?? item.BeatmapID;
int gameplayRulesetId = client.LocalUser.RulesetId ?? item.RulesetID;
RulesetInfo ruleset = rulesets.GetRuleset(gameplayRulesetId)!;
Ruleset rulesetInstance = ruleset.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.QueryBeatmap($@"{nameof(BeatmapInfo.OnlineID)} == $0 AND {nameof(BeatmapInfo.MD5Hash)} == {nameof(BeatmapInfo.OnlineMD5Hash)}", gameplayBeatmapId);
Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap);
Ruleset.Value = ruleset;
Mods.Value = client.LocalUser.Mods.Concat(item.RequiredMods).Select(m => m.ToMod(rulesetInstance)).ToArray();
bool freemods = item.Freestyle || item.AllowedMods.Any();
bool freestyle = item.Freestyle;
if (freemods)
userModsSection.Show();
else
{
userModsSection.Hide();
userModsSelectOverlay.Hide();
}
if (freestyle)
{
userStyleSection.Show();
PlaylistItem apiItem = new PlaylistItem(item).With(beatmap: new Optional<IBeatmapInfo>(new APIBeatmap { OnlineID = gameplayBeatmapId }), ruleset: gameplayRulesetId);
DrawableRoomPlaylistItem? currentDisplay = userStyleDisplayContainer.SingleOrDefault();
if (!apiItem.Equals(currentDisplay?.Item))
{
userStyleDisplayContainer.Child = currentDisplay = new DrawableRoomPlaylistItem(apiItem, true)
{
AllowReordering = false,
RequestEdit = _ => ShowUserStyleSelect()
};
}
currentDisplay.AllowEditing = localBeatmap != null;
}
else
userStyleSection.Hide();
}
/// <summary>
/// Shows the song selection screen to add or edit an item.
/// </summary>
/// <param name="itemToEdit">An optional playlist item to edit. If null, a new item will be added instead.</param>
public void ShowSongSelect(PlaylistItem? itemToEdit = null)
{
if (!this.IsCurrentScreen())
return;
this.Push(new MultiplayerMatchSongSelect(room, itemToEdit));
}
/// <summary>
/// Shows the user mod selection.
/// </summary>
private void showUserModSelect()
{
if (!this.IsCurrentScreen())
return;
userModsSelectOverlay.Show();
}
/// <summary>
/// Shows the user style selection.
/// </summary>
public void ShowUserStyleSelect()
{
if (!this.IsCurrentScreen() || client.Room == null || client.LocalUser == null)
return;
MultiplayerPlaylistItem item = client.Room.CurrentPlaylistItem;
this.Push(new MultiplayerMatchFreestyleSelect(room, new PlaylistItem(item)));
}
/// <summary>
/// Shows the results screen for a playlist item.
/// </summary>
private void showResults(PlaylistItem item)
{
if (!this.IsCurrentScreen() || client.Room == null || client.LocalUser == null)
return;
// 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(client.Room.RoomID, item, client.LocalUser.UserID));
}
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;
client.LeaveRoom().FireAndForget();
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 || !client.IsConnected.Value)
return true;
if (dialogOverlay == null)
return true;
bool hasUnsavedChanges = room.RoomID == null && room.Playlist.Count > 0;
if (hasUnsavedChanges)
{
// 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;
}
if (client.Room != null)
{
if (dialogOverlay.CurrentDialog is ConfirmDialog confirmDialog)
confirmDialog.PerformOkAction();
else
{
dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () =>
{
ExitConfirmed = true;
this.Exit();
}));
}
return false;
}
return true;
}
public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset)
{
if (!this.IsCurrentScreen())
return;
if (client.Room == null || client.LocalUser == null)
return;
if (client.Room.CanAddPlaylistItems(client.LocalUser) != true)
return;
// If there's only one playlist item and we are the host, assume we want to change it. Else add a new one.
PlaylistItem? itemToEdit = client.IsHost && room.Playlist.Count == 1 ? room.Playlist.Single() : null;
ShowSongSelect(itemToEdit);
// Re-run PresentBeatmap now that we've pushed a song select that can handle it.
game?.PresentBeatmap(beatmap.BeatmapSetInfo, b => b.ID == beatmap.BeatmapInfo.ID);
}
// 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 MultiplayerRoomBackgroundScreen();
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
userModsSelectOverlayRegistration?.Dispose();
if (client.IsNotNull())
{
client.RoomUpdated -= onRoomUpdated;
client.SettingsChanged -= onSettingsChanged;
client.ItemChanged -= onItemChanged;
client.UserStyleChanged -= onUserStyleChanged;
client.UserModsChanged -= onUserModsChanged;
client.LoadRequested -= onLoadRequested;
}
}
public partial class AddItemButton : PurpleRoundedButton
{
[Resolved]
private MultiplayerClient client { get; set; } = null!;
protected override void LoadComplete()
{
base.LoadComplete();
client.RoomUpdated += onRoomUpdated;
onRoomUpdated();
}
private void onRoomUpdated()
{
if (client.Room == null || client.LocalUser == null)
return;
Alpha = client.Room.CanAddPlaylistItems(client.LocalUser) ? 1 : 0;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (client.IsNotNull())
client.RoomUpdated -= onRoomUpdated;
}
}
}
}