// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions; 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.Beatmaps; using osu.Game.Database; using osu.Game.Graphics.Cursor; using osu.Game.Online; 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.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Matchmaking.Screens; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Users; namespace osu.Game.Screens.OnlinePlay.Matchmaking { public partial class MatchmakingScreen : OsuScreen { /// /// Padding between rows of the content. /// private const float row_padding = 10; public override bool? ApplyModTrackAdjustments => true; public override bool DisallowExternalBeatmapRulesetChanges => true; public override bool ShowFooter => true; [Cached(typeof(OnlinePlayBeatmapAvailabilityTracker))] private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new MultiplayerBeatmapAvailabilityTracker(); [Resolved] private MultiplayerClient client { get; set; } = null!; [Resolved] private BeatmapManager beatmapManager { get; set; } = null!; [Resolved] private RulesetStore rulesets { get; set; } = null!; [Resolved] private BeatmapLookupCache beatmapLookupCache { get; set; } = null!; [Resolved] private BeatmapModelDownloader beatmapDownloader { get; set; } = null!; [Resolved] private IDialogOverlay dialogOverlay { get; set; } = null!; [Resolved] private AudioManager audio { get; set; } = null!; private readonly MultiplayerRoom room; private Sample? sampleStart; private CancellationTokenSource? downloadCheckCancellation; private int? lastDownloadCheckedBeatmapId; private MatchChatDisplay chat = null!; public MatchmakingScreen(MultiplayerRoom room) { this.room = room; Activity.Value = new UserActivity.InLobby(room); Padding = new MarginPadding { Horizontal = -HORIZONTAL_OVERFLOW_PADDING }; } [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) { 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 GridContainer { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING, Top = row_padding, }, RowDimensions = new[] { new Dimension(), new Dimension(GridSizeMode.Absolute, row_padding), new Dimension(GridSizeMode.AutoSize), }, Content = new Drawable[]?[] { [ new Container { RelativeSizeAxes = Axes.Both, Masking = true, CornerRadius = 10, Children = new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background6, }, new MatchmakingScreenStack(), } } ], null, [ new Container { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Width = 700, Height = 130, Padding = new MarginPadding { Bottom = row_padding }, Child = chat = new MatchChatDisplay(new Room(room)) { RelativeSizeAxes = Axes.Both, } } ] } } } } }; } protected override void LoadComplete() { base.LoadComplete(); client.RoomUpdated += onRoomUpdated; client.UserStateChanged += onUserStateChanged; client.SettingsChanged += onSettingsChanged; client.LoadRequested += onLoadRequested; beatmapAvailabilityTracker.Availability.BindValueChanged(onBeatmapAvailabilityChanged, true); Footer!.Add(chat.CreateProxy()); } private void onRoomUpdated() { if (this.IsCurrentScreen() && client.Room == null) { Logger.Log($"{this} exiting due to loss of room or connection"); this.Exit(); } } private void onUserStateChanged(MultiplayerRoomUser user, MultiplayerUserState state) { if (user.Equals(client.LocalUser) && state == MultiplayerUserState.Idle) this.MakeCurrent(); } private void onSettingsChanged(MultiplayerRoomSettings _) => Scheduler.Add(() => { checkForAutomaticDownload(); updateGameplayState(); }); private void onBeatmapAvailabilityChanged(ValueChangedEvent e) => Scheduler.Add(() => { if (client.Room == null || client.LocalUser == null) return; client.ChangeBeatmapAvailability(e.NewValue).FireAndForget(); switch (e.NewValue.State) { case DownloadState.NotDownloaded: case DownloadState.LocallyAvailable: updateGameplayState(); break; } }); private void updateGameplayState() { MultiplayerPlaylistItem item = client.Room!.CurrentPlaylistItem; if (item.Expired) return; RulesetInfo ruleset = rulesets.GetRuleset(item.RulesetID)!; 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)}", item.BeatmapID); Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); Ruleset.Value = ruleset; Mods.Value = item.RequiredMods.Select(m => m.ToMod(rulesetInstance)).ToArray(); if (Beatmap.Value is DummyWorkingBeatmap) { if (client.LocalUser!.State == MultiplayerUserState.Ready) client.ChangeState(MultiplayerUserState.Idle).FireAndForget(); } else { if (client.LocalUser!.State == MultiplayerUserState.Idle) client.ChangeState(MultiplayerUserState.Ready).FireAndForget(); } client.ChangeBeatmapAvailability(beatmapAvailabilityTracker.Availability.Value).FireAndForget(); } private void onLoadRequested() => Scheduler.Add(() => { updateGameplayState(); if (Beatmap.IsDefault) { Logger.Log("Aborting gameplay start - beatmap not downloaded."); return; } sampleStart?.Play(); this.Push(new MultiplayerPlayerLoader(() => new MatchmakingPlayer(new Room(room), new PlaylistItem(client.Room!.CurrentPlaylistItem), room.Users.ToArray()))); }); private void checkForAutomaticDownload() { if (client.Room == null) return; MultiplayerPlaylistItem item = client.Room.CurrentPlaylistItem; // This method is called every time anything changes in the room. // This could result in download requests firing far too often, when we only expect them to fire once per beatmap. // // Without this check, we would see especially egregious behaviour when a user has hit the download rate limit. if (lastDownloadCheckedBeatmapId == item.BeatmapID) return; lastDownloadCheckedBeatmapId = item.BeatmapID; downloadCheckCancellation?.Cancel(); // In a perfect world we'd use BeatmapAvailability, but there's no event-driven flow for when a selection changes. // ie. if selection changes from "not downloaded" to another "not downloaded" we wouldn't get a value changed raised. beatmapLookupCache .GetBeatmapAsync(item.BeatmapID, (downloadCheckCancellation = new CancellationTokenSource()).Token) .ContinueWith(resolved => Schedule(() => { var beatmapSet = resolved.GetResultSafely()?.BeatmapSet; if (beatmapSet == null) return; if (beatmapManager.IsAvailableLocally(new BeatmapSetInfo { OnlineID = beatmapSet.OnlineID })) return; beatmapDownloader.Download(beatmapSet); })); } private bool exitConfirmed; public override bool OnExiting(ScreenExitEvent e) { if (base.OnExiting(e)) return true; if (exitConfirmed) { client.LeaveRoom().FireAndForget(); return false; } 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; if (this.IsCurrentScreen()) this.Exit(); })); } return true; } public override void OnResuming(ScreenTransitionEvent e) { base.OnResuming(e); if (e.Last is not MultiplayerPlayerLoader playerLoader) return; if (!playerLoader.GameplayPassed) { client.AbortGameplay().FireAndForget(); return; } client.ChangeState(MultiplayerUserState.Idle); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); if (client.IsNotNull()) { client.RoomUpdated -= onRoomUpdated; client.UserStateChanged -= onUserStateChanged; client.SettingsChanged -= onSettingsChanged; client.LoadRequested -= onLoadRequested; } } } }