1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-13 20:33:35 +08:00
Files
osu-lazer/osu.Game/Screens/OnlinePlay/Matchmaking/Queue/ScreenQueue.cs
T

781 lines
32 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.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework;
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.Audio;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Matchmaking;
using osu.Game.Online.Matchmaking.Requests;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
using osu.Game.Overlays;
using osu.Game.Overlays.Volume;
using osu.Game.Rulesets;
using osu.Game.Screens.Footer;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Queue
{
/// <summary>
/// The initial screen that users arrive at when preparing for a quick play session.
/// </summary>
public partial class ScreenQueue : OsuScreen
{
public override bool ShowFooter => true;
public override bool? ApplyModTrackAdjustments => false;
private Container mainContent = null!;
private CloudVisualisation cloud = null!;
private RatingDistributionGraph ratingGraph = null!;
private FillFlowContainer resultPanelContainer = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private MultiplayerClient client { get; set; } = null!;
[Resolved]
private QueueController queue { get; set; } = null!;
[Resolved]
private UserLookupCache userLookupCache { get; set; } = null!;
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
[Resolved]
private MusicController music { get; set; } = null!;
[Resolved]
private DashboardOverlay? dashboardOverlay { get; set; }
private readonly IBindable<MatchmakingScreenState> currentState = new Bindable<MatchmakingScreenState>();
private readonly Bindable<MatchmakingPool[]?> availablePools = new Bindable<MatchmakingPool[]?>();
private readonly Bindable<MatchmakingPool?> selectedPool = new Bindable<MatchmakingPool?>();
private readonly MatchmakingPoolType poolType;
private CancellationTokenSource userLookupCancellation = new CancellationTokenSource();
private Sample? enqueueSample;
private Sample? matchFoundSample;
private SampleChannel? waitingLoopChannel;
private ScheduledDelegate? startLoopPlaybackDelegate;
private DrawableSample waitingLoop = null!;
private ScheduledDelegate? pushScreenDelegate;
private int? userRating;
private GridContainer mainGrid = null!;
private IBindable<bool> isConnected = null!;
public ScreenQueue(MatchmakingPoolType poolType)
{
this.poolType = poolType;
}
[BackgroundDependencyLoader]
private void load(AudioManager audio, IAPIProvider api)
{
enqueueSample = audio.Samples.Get(@"Multiplayer/Matchmaking/enqueue");
matchFoundSample = audio.Samples.Get(@"Multiplayer/Matchmaking/match-found");
InternalChild = new InverseScalingDrawSizePreservingFillContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
waitingLoop = new DrawableSample(audio.Samples.Get(@"Multiplayer/Matchmaking/waiting-loop")),
new GlobalScrollAdjustsVolume(),
mainGrid = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Horizontal = 20,
Top = 20,
Bottom = ScreenFooter.HEIGHT + 20
},
RowDimensions =
[
new Dimension(),
new Dimension(GridSizeMode.Relative, RuntimeInfo.IsMobile ? 0.55f : 0.35f)
],
Content = new[]
{
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(5),
Child = new Container
{
RelativeSizeAxes = Axes.Both,
CornerRadius = 10f,
Masking = true,
Children = new Drawable[]
{
new PanelBackground(),
new GridContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(10),
RowDimensions =
[
new Dimension(GridSizeMode.AutoSize)
],
Content = new[]
{
new Drawable[] { new QueueSectionHeader("Queued players") },
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
cloud = new CloudVisualisation
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.6f)
},
new MatchmakingAvatar(api.LocalUser.Value, true)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(3),
}
}
}
}
}
}
}
}
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(5),
Child = new Container
{
RelativeSizeAxes = Axes.Both,
CornerRadius = 10f,
Masking = true,
Children = new Drawable[]
{
new PanelBackground(),
new GridContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(10) { Bottom = 0 },
RowDimensions =
[
new Dimension(GridSizeMode.AutoSize)
],
Content = new[]
{
new Drawable[] { new QueueSectionHeader("Recent Matches") },
new Drawable[]
{
new OsuScrollContainer(Direction.Vertical)
{
RelativeSizeAxes = Axes.Both,
ScrollbarOverlapsContent = false,
Child = resultPanelContainer = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(10),
}
}
}
}
}
}
}
}
},
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(5),
Child = new Container
{
RelativeSizeAxes = Axes.Both,
CornerRadius = 10f,
Masking = true,
Children = new Drawable[]
{
new PanelBackground(),
new GridContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(10),
RowDimensions =
[
new Dimension(GridSizeMode.AutoSize)
],
Content = new[]
{
new Drawable[] { new QueueSectionHeader("Queues") },
new Drawable[]
{
mainContent = new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(20),
Alpha = 0,
},
}
}
}
}
}
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(5),
Child = new Container
{
RelativeSizeAxes = Axes.Both,
CornerRadius = 10f,
Masking = true,
Children = new Drawable[]
{
new PanelBackground(),
new GridContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(10),
RowDimensions =
[
new Dimension(GridSizeMode.AutoSize)
],
Content = new[]
{
new Drawable[] { new QueueSectionHeader("Ratings") },
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = -10 },
Child = ratingGraph = new RatingDistributionGraph
{
RelativeSizeAxes = Axes.Both,
}
}
}
}
}
}
}
}
}
}
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
int delay = 0;
foreach (var a in mainGrid.Content)
{
foreach (var d in a)
{
d.FadeOut()
.Delay(delay)
.FadeInFromZero(500, Easing.OutQuint);
delay += 100;
}
}
client.MatchmakingLobbyStatusChanged += onMatchmakingLobbyStatusChanged;
currentState.BindTo(queue.CurrentState);
currentState.BindValueChanged(s => SetState(s.NewValue));
selectedPool.BindTo(queue.SelectedPool);
selectedPool.BindValueChanged(e => refreshLobbyData());
isConnected = client.IsConnected.GetBoundCopy();
isConnected.BindValueChanged(connected => Schedule(() =>
{
if (connected.NewValue)
{
populateAvailablePools().FireAndForget();
refreshLobbyData();
}
else
{
availablePools.Value = null;
clearLobbyData();
}
}), true);
}
private async Task populateAvailablePools()
{
MatchmakingPool[] pools = await client.GetMatchmakingPoolsOfType(poolType).ConfigureAwait(false);
Schedule(() =>
{
availablePools.Value = pools;
// Default to the currently queueing pool, or fallback to the user's ruleset for the initial pool selection.
selectedPool.Value ??= pools.FirstOrDefault(p => p.RulesetId == ruleset.Value.OnlineID) ?? pools.FirstOrDefault();
});
}
private void onMatchmakingLobbyStatusChanged(MatchmakingLobbyStatus status) => Scheduler.Add(() =>
{
userLookupCancellation.Cancel();
var cancellation = userLookupCancellation = new CancellationTokenSource();
userLookupCache.GetUsersAsync(status.UsersInQueue, cancellation.Token)
.ContinueWith(result => Schedule(() =>
{
APIUser?[] users = result.GetResultSafely();
if (!cancellation.IsCancellationRequested)
cloud.Users = users.OfType<APIUser>().ToArray();
}), cancellation.Token);
// Global (incremental) updates will not contain the user rating, so keep the one we already received from initial status data.
if (status.UserRating != null)
userRating = status.UserRating;
ratingGraph.SetData(status.RatingDistribution, userRating);
loadRecentMatches(status.RecentMatches.OfType<RankedPlayRoomState>().ToArray()).FireAndForget();
});
private async Task loadRecentMatches(RankedPlayRoomState[] matches)
{
await userLookupCache.GetUsersAsync(matches.SelectMany(m => m.Users.Keys).ToArray()).ConfigureAwait(false);
Scheduler.Add(() =>
{
foreach (var match in matches)
{
resultPanelContainer.Insert(-resultPanelContainer.Count, new RankedPlayMatchPanel(match)
{
RelativeSizeAxes = Axes.X,
Width = 0.48f
});
}
if (resultPanelContainer.Any(c => c.Position != Vector2.Zero))
{
resultPanelContainer.LayoutDuration = 400;
resultPanelContainer.LayoutEasing = Easing.OutQuint;
}
});
}
private void refreshLobbyData()
{
clearLobbyData();
if (selectedPool.Value == null)
{
client.MatchmakingLeaveLobby().FireAndForget();
return;
}
client.MatchmakingJoinLobbyWithParams(new MatchmakingJoinLobbyRequest
{
PoolId = selectedPool.Value.Id
}).FireAndForget();
}
private void clearLobbyData()
{
resultPanelContainer.Clear();
resultPanelContainer.LayoutDuration = 0;
userRating = null;
ratingGraph.SetData([], null);
cloud.Users = Array.Empty<APIUser>();
}
public override void OnEntering(ScreenTransitionEvent e)
{
base.OnEntering(e);
queue.SearchInForeground();
using (BeginDelayedSequence(800))
Schedule(() => SetState(currentState.Value));
}
public override void OnResuming(ScreenTransitionEvent e)
{
base.OnResuming(e);
// Rejoin the lobby.
selectedPool.TriggerChange();
}
public override void OnSuspending(ScreenTransitionEvent e)
{
base.OnSuspending(e);
stopWaitingLoopPlayback();
client.MatchmakingLeaveLobby().FireAndForget();
}
public override bool OnExiting(ScreenExitEvent e)
{
if (base.OnExiting(e))
return true;
stopWaitingLoopPlayback();
switch (currentState.Value)
{
default:
client.MatchmakingLeaveLobby().FireAndForget();
queue.SearchInBackground();
return false;
case MatchmakingScreenState.PendingAccept:
case MatchmakingScreenState.AcceptedWaitingForRoom:
queue.LeaveQueue();
return true;
case MatchmakingScreenState.InRoom:
// Block exit until it's initiated from inside the matchmaking screen.
return true;
}
}
public void SetState(MatchmakingScreenState newState)
{
mainContent.FadeInFromZero(500, Easing.OutQuint);
mainContent.Clear();
startLoopPlaybackDelegate?.Cancel();
stopWaitingLoopPlayback();
pushScreenDelegate?.Cancel();
pushScreenDelegate = null;
switch (newState)
{
case MatchmakingScreenState.Idle:
LinkFlowContainer duelHint;
mainContent.Child = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(10),
Children = new Drawable[]
{
new PoolSelector
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AvailablePools = { BindTarget = availablePools },
SelectedPool = { BindTarget = selectedPool }
},
new BeginQueueingButton
{
DarkerColour = colours.Blue2,
LighterColour = colours.Blue1,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Width = 200,
Enabled = { BindTarget = isConnected },
SelectedPool = { BindTarget = selectedPool },
Action = () =>
{
Debug.Assert(selectedPool.Value != null);
queue.JoinQueue(selectedPool.Value);
},
Text = "Begin queueing",
},
duelHint = new LinkFlowContainer
{
TextAnchor = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
}
}
};
duelHint.AddText("Open the ");
duelHint.AddLink("dashboard", () => dashboardOverlay?.Show());
duelHint.AddText(" to duel another player!");
break;
case MatchmakingScreenState.Queueing:
mainContent.Child = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(15),
Children = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Waiting for a game...",
Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate),
},
new LoadingSpinner
{
State = { Value = Visibility.Visible },
},
new ShearedButton
{
DarkerColour = colours.Red3,
LighterColour = colours.Red4,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 200,
Text = "Stop queueing",
Action = () => queue.LeaveQueue()
}
}
};
enqueueSample?.Play();
startLoopPlaybackDelegate = Scheduler.AddDelayed(startWaitingLoopPlayback, 2000);
break;
case MatchmakingScreenState.PendingAccept:
client.MatchmakingAcceptInvitation().FireAndForget();
SetState(MatchmakingScreenState.AcceptedWaitingForRoom);
matchFoundSample?.Play();
music.DuckMomentarily(1250);
break;
case MatchmakingScreenState.AcceptedWaitingForRoom:
mainContent.Child = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(20),
Children = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Waiting for opponents...",
Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate),
},
new LoadingSpinner
{
State = { Value = Visibility.Visible },
},
}
};
startWaitingLoopPlayback();
break;
case MatchmakingScreenState.InRoom:
// room received, show users and transition to next screen.
mainContent.Child = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(20),
Children = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Good luck!",
Font = OsuFont.GetFont(size: 32, weight: FontWeight.Light, typeface: Typeface.TorusAlternate),
},
}
};
using (BeginDelayedSequence(2000))
{
pushScreenDelegate = Schedule(() =>
{
switch (poolType)
{
case MatchmakingPoolType.QuickPlay:
this.Push(new ScreenMatchmaking(client.Room!));
break;
case MatchmakingPoolType.RankedPlay:
this.Push(new RankedPlayScreen(client.Room!));
break;
}
});
}
break;
default:
throw new ArgumentOutOfRangeException(nameof(newState), newState, null);
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
stopWaitingLoopPlayback();
if (client.IsNotNull())
client.MatchmakingLobbyStatusChanged -= onMatchmakingLobbyStatusChanged;
}
public enum MatchmakingScreenState
{
Idle,
Queueing,
PendingAccept,
AcceptedWaitingForRoom,
InRoom
}
private void startWaitingLoopPlayback()
{
stopWaitingLoopPlayback();
waitingLoopChannel = waitingLoop.GetChannel();
if (waitingLoopChannel == null)
return;
waitingLoopChannel.Looping = true;
waitingLoopChannel?.Play();
waitingLoop.VolumeTo(1)
.Delay(2000)
.VolumeTo(0, 12000);
}
private void stopWaitingLoopPlayback()
{
waitingLoopChannel?.Stop();
waitingLoopChannel?.Dispose();
}
public partial class PanelBackground : CompositeDrawable
{
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.Both;
InternalChild = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background3,
Blending = BlendingParameters.Additive,
Alpha = 0.3f,
};
}
}
public partial class QueueSectionHeader : SectionHeader
{
public QueueSectionHeader(string header)
: base(header)
{
// Reduce base class padding.
Margin = new MarginPadding { Top = 5, Bottom = 10, Horizontal = 5 };
}
}
private partial class BeginQueueingButton : SelectionButton
{
public readonly IBindable<MatchmakingPool?> SelectedPool = new Bindable<MatchmakingPool?>();
protected override void LoadComplete()
{
base.LoadComplete();
SelectedPool.BindValueChanged(p => Enabled.Value = p.NewValue != null, true);
}
}
private partial class SelectionButton : ShearedButton, IKeyBindingHandler<GlobalAction>
{
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Action == GlobalAction.Select && !e.Repeat)
{
TriggerClickWithSound();
return true;
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
}
}
}