2020-12-20 23:04:06 +08:00
// 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.
2022-06-17 15:37:17 +08:00
#nullable disable
2020-12-20 23:13:05 +08:00
using System ;
using System.Diagnostics ;
2021-08-09 17:13:28 +08:00
using System.Linq ;
2020-12-20 23:13:05 +08:00
using System.Threading.Tasks ;
using osu.Framework.Allocation ;
2020-12-23 14:58:50 +08:00
using osu.Framework.Bindables ;
2021-08-09 17:13:28 +08:00
using osu.Framework.Graphics ;
using osu.Framework.Graphics.Containers ;
2020-12-22 13:59:11 +08:00
using osu.Framework.Logging ;
2022-03-10 19:02:58 +08:00
using osu.Framework.Screens ;
2020-12-24 14:32:55 +08:00
using osu.Game.Graphics.UserInterface ;
2020-12-20 23:04:06 +08:00
using osu.Game.Online.Multiplayer ;
2020-12-25 12:38:11 +08:00
using osu.Game.Online.Rooms ;
2020-12-20 23:13:05 +08:00
using osu.Game.Scoring ;
2020-12-23 16:39:08 +08:00
using osu.Game.Screens.Play ;
2020-12-22 18:09:59 +08:00
using osu.Game.Screens.Play.HUD ;
2020-12-20 23:13:05 +08:00
using osu.Game.Screens.Ranking ;
2021-08-22 09:54:07 +08:00
using osu.Game.Users ;
2020-12-22 18:09:59 +08:00
using osuTK ;
2020-12-20 23:04:06 +08:00
2020-12-25 23:50:00 +08:00
namespace osu.Game.Screens.OnlinePlay.Multiplayer
2020-12-20 23:04:06 +08:00
{
2021-03-23 14:33:31 +08:00
public class MultiplayerPlayer : RoomSubmittingPlayer
2020-12-20 23:04:06 +08:00
{
2020-12-20 23:13:05 +08:00
protected override bool PauseOnFocusLost = > false ;
// Disallow fails in multiplayer for now.
protected override bool CheckModsAllowFailure ( ) = > false ;
2021-08-22 09:54:07 +08:00
protected override UserActivity InitialActivity = > new UserActivity . InMultiplayerGame ( Beatmap . Value . BeatmapInfo , Ruleset . Value ) ;
2020-12-20 23:13:05 +08:00
[Resolved]
2021-05-20 14:39:45 +08:00
private MultiplayerClient client { get ; set ; }
2020-12-20 23:13:05 +08:00
2020-12-23 18:57:43 +08:00
private IBindable < bool > isConnected ;
2020-12-20 23:13:05 +08:00
private readonly TaskCompletionSource < bool > resultsReady = new TaskCompletionSource < bool > ( ) ;
2020-12-22 18:09:59 +08:00
private MultiplayerGameplayLeaderboard leaderboard ;
2020-12-23 14:58:50 +08:00
2021-08-10 17:39:20 +08:00
private readonly MultiplayerRoomUser [ ] users ;
2020-12-24 09:38:53 +08:00
2022-05-11 15:12:54 +08:00
private readonly Bindable < bool > leaderboardExpanded = new BindableBool ( ) ;
2020-12-24 14:32:55 +08:00
private LoadingLayer loadingDisplay ;
2021-08-09 17:13:28 +08:00
private FillFlowContainer leaderboardFlow ;
2020-12-24 14:32:55 +08:00
2020-12-24 09:38:53 +08:00
/// <summary>
/// Construct a multiplayer player.
/// </summary>
2021-08-24 12:22:06 +08:00
/// <param name="room">The room.</param>
2020-12-24 09:38:53 +08:00
/// <param name="playlistItem">The playlist item to be played.</param>
2021-08-10 17:39:20 +08:00
/// <param name="users">The users which are participating in this game.</param>
2021-08-24 12:22:06 +08:00
public MultiplayerPlayer ( Room room , PlaylistItem playlistItem , MultiplayerRoomUser [ ] users )
: base ( room , playlistItem , new PlayerConfiguration
2020-12-23 16:39:08 +08:00
{
AllowPause = false ,
AllowRestart = false ,
2021-04-16 12:59:10 +08:00
AllowSkipping = false ,
2020-12-23 16:39:08 +08:00
} )
2020-12-20 23:13:05 +08:00
{
2021-08-10 17:39:20 +08:00
this . users = users ;
2020-12-20 23:13:05 +08:00
}
[BackgroundDependencyLoader]
private void load ( )
2020-12-20 23:04:06 +08:00
{
2021-08-05 18:01:07 +08:00
if ( ! LoadedBeatmapSuccessfully )
return ;
2021-08-09 17:13:28 +08:00
HUDOverlay . Add ( leaderboardFlow = new FillFlowContainer
{
AutoSizeAxes = Axes . Both ,
Direction = FillDirection . Vertical ,
2021-08-17 13:57:48 +08:00
Spacing = new Vector2 ( 5 )
2021-08-09 17:13:28 +08:00
} ) ;
2022-05-11 15:12:54 +08:00
HUDOverlay . HoldingForHUD . BindValueChanged ( _ = > updateLeaderboardExpandedState ( ) ) ;
LocalUserPlaying . BindValueChanged ( _ = > updateLeaderboardExpandedState ( ) , true ) ;
2020-12-24 21:32:30 +08:00
// todo: this should be implemented via a custom HUD implementation, and correctly masked to the main content area.
2022-05-30 18:18:38 +08:00
LoadComponentAsync ( leaderboard = new MultiplayerGameplayLeaderboard ( users ) , l = >
2021-08-09 17:13:28 +08:00
{
if ( ! LoadedBeatmapSuccessfully )
return ;
2022-05-11 15:12:54 +08:00
leaderboard . Expanded . BindTo ( leaderboardExpanded ) ;
2021-08-09 17:13:28 +08:00
2021-08-17 13:57:48 +08:00
leaderboardFlow . Insert ( 0 , l ) ;
2021-08-09 17:13:28 +08:00
if ( leaderboard . TeamScores . Count > = 2 )
{
LoadComponentAsync ( new GameplayMatchScoreDisplay
{
Team1Score = { BindTarget = leaderboard . TeamScores . First ( ) . Value } ,
Team2Score = { BindTarget = leaderboard . TeamScores . Last ( ) . Value } ,
Expanded = { BindTarget = HUDOverlay . ShowHud } ,
2021-08-17 13:57:48 +08:00
} , scoreDisplay = > leaderboardFlow . Insert ( 1 , scoreDisplay ) ) ;
2021-08-09 17:13:28 +08:00
}
} ) ;
2020-12-24 21:32:30 +08:00
2021-08-24 12:22:06 +08:00
LoadComponentAsync ( new GameplayChatDisplay ( Room )
2021-08-17 13:57:48 +08:00
{
2022-05-11 15:12:54 +08:00
Expanded = { BindTarget = leaderboardExpanded } ,
2021-08-17 13:57:48 +08:00
} , chat = > leaderboardFlow . Insert ( 2 , chat ) ) ;
2021-01-04 21:42:39 +08:00
HUDOverlay . Add ( loadingDisplay = new LoadingLayer ( true ) { Depth = float . MaxValue } ) ;
2021-03-23 15:05:40 +08:00
}
protected override void LoadAsyncComplete ( )
{
base . LoadAsyncComplete ( ) ;
2020-12-24 21:32:30 +08:00
2021-08-05 18:01:07 +08:00
if ( ! LoadedBeatmapSuccessfully )
return ;
2021-03-24 12:20:44 +08:00
if ( ! ValidForResume )
return ; // token retrieval may have failed.
2020-12-20 23:13:05 +08:00
2022-04-21 21:37:26 +08:00
client . GameplayStarted + = onGameplayStarted ;
2020-12-20 23:13:05 +08:00
client . ResultsReady + = onResultsReady ;
2020-12-24 14:32:55 +08:00
ScoreProcessor . HasCompleted . BindValueChanged ( completed = >
{
// wait for server to tell us that results are ready (see SubmitScore implementation)
loadingDisplay . Show ( ) ;
} ) ;
2020-12-23 14:58:50 +08:00
isConnected = client . IsConnected . GetBoundCopy ( ) ;
2021-01-12 18:04:16 +08:00
isConnected . BindValueChanged ( connected = > Schedule ( ( ) = >
2020-12-22 13:59:11 +08:00
{
2020-12-23 14:58:50 +08:00
if ( ! connected . NewValue )
2020-12-22 13:59:11 +08:00
{
2020-12-25 12:38:11 +08:00
// messaging to the user about this disconnect will be provided by the MultiplayerMatchSubScreen.
2020-12-23 15:51:11 +08:00
failAndBail ( ) ;
2020-12-23 15:32:58 +08:00
}
2021-01-12 18:04:16 +08:00
} ) , true ) ;
2022-04-08 13:53:14 +08:00
}
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
2020-12-23 14:58:50 +08:00
2020-12-22 18:09:59 +08:00
Debug . Assert ( client . Room ! = null ) ;
2020-12-24 14:32:55 +08:00
}
protected override void StartGameplay ( )
{
2022-05-31 18:58:44 +08:00
// We can enter this screen one of two ways:
// 1. Via the automatic natural progression of PlayerLoader into Player.
// We'll arrive here in a Loaded state, and we need to let the server know that we're ready to start.
// 2. Via the server forcefully starting gameplay because players have been hanging out in PlayerLoader for too long.
// We'll arrive here in a Playing state, and we should neither show the loading spinner nor tell the server that we're ready to start (gameplay has already started).
//
// The base call is blocked here because in both cases gameplay is started only when the server says so via onGameplayStarted().
2022-04-21 21:55:13 +08:00
if ( client . LocalUser ? . State = = MultiplayerUserState . Loaded )
{
loadingDisplay . Show ( ) ;
client . ChangeState ( MultiplayerUserState . ReadyForGameplay ) ;
}
2020-12-23 15:51:11 +08:00
}
2022-05-11 15:12:54 +08:00
private void updateLeaderboardExpandedState ( ) = >
leaderboardExpanded . Value = ! LocalUserPlaying . Value | | HUDOverlay . HoldingForHUD . Value ;
2020-12-23 15:51:11 +08:00
private void failAndBail ( string message = null )
{
if ( ! string . IsNullOrEmpty ( message ) )
Logger . Log ( message , LoggingTarget . Runtime , LogLevel . Important ) ;
2020-12-22 13:59:11 +08:00
2020-12-24 00:37:49 +08:00
Schedule ( ( ) = > PerformExit ( false ) ) ;
2020-12-20 23:13:05 +08:00
}
2020-12-22 18:09:59 +08:00
protected override void Update ( )
{
base . Update ( ) ;
2021-08-05 18:01:07 +08:00
if ( ! LoadedBeatmapSuccessfully )
return ;
2020-12-23 05:31:40 +08:00
adjustLeaderboardPosition ( ) ;
}
private void adjustLeaderboardPosition ( )
{
2020-12-22 18:09:59 +08:00
const float padding = 44 ; // enough margin to avoid the hit error display.
2021-08-09 17:13:28 +08:00
leaderboardFlow . Position = new Vector2 ( padding , padding + HUDOverlay . TopScoringElementsHeight ) ;
2020-12-20 23:13:05 +08:00
}
2022-04-21 21:37:26 +08:00
private void onGameplayStarted ( ) = > Scheduler . Add ( ( ) = >
2020-12-24 14:32:55 +08:00
{
2022-03-10 19:02:58 +08:00
if ( ! this . IsCurrentScreen ( ) )
return ;
2020-12-24 14:32:55 +08:00
loadingDisplay . Hide ( ) ;
base . StartGameplay ( ) ;
} ) ;
2020-12-20 23:13:05 +08:00
2022-03-10 18:52:08 +08:00
private void onResultsReady ( )
2022-03-10 17:50:53 +08:00
{
2022-03-10 18:52:08 +08:00
// Schedule is required to ensure that `TaskCompletionSource.SetResult` is not called more than once.
// A scenario where this can occur is if this instance is not immediately disposed (ie. async disposal queue).
2022-03-10 19:02:58 +08:00
Schedule ( ( ) = >
{
if ( ! this . IsCurrentScreen ( ) )
return ;
resultsReady . SetResult ( true ) ;
} ) ;
2022-03-10 18:52:08 +08:00
}
2020-12-20 23:13:05 +08:00
2021-03-23 14:45:22 +08:00
protected override async Task PrepareScoreForResultsAsync ( Score score )
2020-12-20 23:13:05 +08:00
{
2021-03-23 14:45:22 +08:00
await base . PrepareScoreForResultsAsync ( score ) . ConfigureAwait ( false ) ;
2020-12-20 23:13:05 +08:00
2021-03-08 11:57:16 +08:00
await client . ChangeState ( MultiplayerUserState . FinishedPlay ) . ConfigureAwait ( false ) ;
2020-12-20 23:13:05 +08:00
2020-12-24 18:33:49 +08:00
// Await up to 60 seconds for results to become available (6 api request timeouts).
2020-12-20 23:13:05 +08:00
// This is arbitrary just to not leave the player in an essentially deadlocked state if any connection issues occur.
2021-03-08 11:57:16 +08:00
await Task . WhenAny ( resultsReady . Task , Task . Delay ( TimeSpan . FromSeconds ( 60 ) ) ) . ConfigureAwait ( false ) ;
2020-12-20 23:13:05 +08:00
}
protected override ResultsScreen CreateResults ( ScoreInfo score )
{
2021-08-24 13:25:29 +08:00
Debug . Assert ( Room . RoomID . Value ! = null ) ;
2021-08-13 02:01:31 +08:00
return leaderboard . TeamScores . Count = = 2
2021-08-24 13:25:29 +08:00
? new MultiplayerTeamResultsScreen ( score , Room . RoomID . Value . Value , PlaylistItem , leaderboard . TeamScores )
: new MultiplayerResultsScreen ( score , Room . RoomID . Value . Value , PlaylistItem ) ;
2020-12-20 23:13:05 +08:00
}
protected override void Dispose ( bool isDisposing )
{
base . Dispose ( isDisposing ) ;
if ( client ! = null )
{
2022-04-21 21:37:26 +08:00
client . GameplayStarted - = onGameplayStarted ;
2020-12-20 23:13:05 +08:00
client . ResultsReady - = onResultsReady ;
}
2020-12-20 23:04:06 +08:00
}
}
}