2022-06-07 05:40:21 +00: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.
#nullable enable
using System ;
2022-06-07 09:11:54 +00:00
using System.Diagnostics ;
2022-06-07 11:28:42 +00:00
using System.Linq ;
2022-06-07 05:40:21 +00:00
using osu.Framework.Allocation ;
2022-06-10 07:22:34 +00:00
using osu.Framework.Bindables ;
2022-06-07 05:40:21 +00:00
using osu.Framework.Configuration ;
using osu.Framework.Extensions.Color4Extensions ;
using osu.Framework.Graphics ;
using osu.Framework.Graphics.Colour ;
using osu.Framework.Graphics.Containers ;
2022-06-07 13:52:24 +00:00
using osu.Framework.Graphics.Effects ;
2022-06-07 05:40:21 +00:00
using osu.Framework.Graphics.Shapes ;
2022-06-07 11:02:26 +00:00
using osu.Framework.Graphics.Sprites ;
2022-06-07 05:40:21 +00:00
using osu.Framework.Input.Events ;
2022-06-07 10:03:26 +00:00
using osu.Framework.Platform ;
using osu.Framework.Platform.Windows ;
2022-06-07 05:40:21 +00:00
using osu.Framework.Screens ;
using osu.Framework.Utils ;
using osu.Game.Graphics ;
using osu.Game.Graphics.Containers ;
2022-06-07 13:52:24 +00:00
using osu.Game.Graphics.Sprites ;
2022-06-07 05:40:21 +00:00
using osu.Game.Overlays ;
using osuTK ;
using osuTK.Input ;
2022-06-07 14:10:08 +00:00
namespace osu.Game.Screens.Utility
2022-06-07 05:40:21 +00:00
{
2022-06-07 14:34:19 +00:00
public class LatencyCertifierScreen : OsuScreen
2022-06-07 05:40:21 +00:00
{
private FrameSync previousFrameSyncMode ;
2022-06-07 10:03:26 +00:00
private double previousActiveHz ;
2022-06-07 05:40:21 +00:00
2022-06-07 05:51:16 +00:00
private readonly OsuTextFlowContainer statusText ;
2022-06-07 05:40:21 +00:00
public override bool HideOverlaysOnEnter = > true ;
2022-06-07 10:31:56 +00:00
public override bool CursorVisible = > mainArea . Count = = 0 ;
2022-06-07 05:40:21 +00:00
public override float BackgroundParallaxAmount = > 0 ;
2022-06-07 06:21:19 +00:00
private readonly OsuTextFlowContainer explanatoryText ;
2022-06-07 11:28:42 +00:00
private readonly Container < LatencyArea > mainArea ;
2022-06-07 05:51:16 +00:00
private readonly Container resultsArea ;
2022-06-07 05:40:21 +00:00
2022-06-07 10:03:26 +00:00
/// <summary>
/// The rate at which the game host should attempt to run.
/// </summary>
private const int target_host_update_frames = 4000 ;
2022-06-07 05:40:21 +00:00
[Cached]
private readonly OverlayColourProvider overlayColourProvider = new OverlayColourProvider ( OverlayColourScheme . Orange ) ;
2022-06-07 09:11:54 +00:00
[Resolved]
private OsuColour colours { get ; set ; } = null ! ;
2022-06-07 05:40:21 +00:00
[Resolved]
private FrameworkConfigManager config { get ; set ; } = null ! ;
2022-06-10 07:22:34 +00:00
private readonly Bindable < LatencyVisualMode > visualMode = new Bindable < LatencyVisualMode > ( ) ;
2022-06-07 09:11:54 +00:00
private const int rounds_to_complete = 5 ;
2022-06-07 13:52:24 +00:00
private const int rounds_to_complete_certified = 20 ;
2022-06-10 06:19:10 +00:00
/// <summary>
/// Whether we are now in certification mode and decreasing difficulty.
/// </summary>
private bool isCertifying ;
private int totalRoundForNextResultsScreen = > isCertifying ? rounds_to_complete_certified : rounds_to_complete ;
2022-06-10 05:23:24 +00:00
private int attemptsAtCurrentDifficulty ;
private int correctAtCurrentDifficulty ;
2022-06-07 09:11:54 +00:00
2022-06-10 06:19:10 +00:00
public int DifficultyLevel { get ; private set ; } = 1 ;
2022-06-07 09:11:54 +00:00
2022-06-07 10:43:33 +00:00
private double lastPoll ;
private int pollingMax ;
2022-06-07 10:03:26 +00:00
[Resolved]
private GameHost host { get ; set ; } = null ! ;
2022-06-07 14:34:19 +00:00
public LatencyCertifierScreen ( )
2022-06-07 05:40:21 +00:00
{
InternalChildren = new Drawable [ ]
{
2022-06-07 05:51:16 +00:00
new Box
{
Colour = overlayColourProvider . Background6 ,
RelativeSizeAxes = Axes . Both ,
} ,
2022-06-07 11:28:42 +00:00
mainArea = new Container < LatencyArea >
2022-06-07 05:40:21 +00:00
{
RelativeSizeAxes = Axes . Both ,
} ,
// Make sure the edge between the two comparisons can't be used to ascertain latency.
new Box
{
Name = "separator" ,
Colour = ColourInfo . GradientHorizontal ( overlayColourProvider . Background6 , overlayColourProvider . Background6 . Opacity ( 0 ) ) ,
2022-06-07 11:42:19 +00:00
Width = 100 ,
2022-06-07 05:40:21 +00:00
RelativeSizeAxes = Axes . Y ,
Anchor = Anchor . TopCentre ,
Origin = Anchor . TopLeft ,
} ,
new Box
{
Name = "separator" ,
Colour = ColourInfo . GradientHorizontal ( overlayColourProvider . Background6 . Opacity ( 0 ) , overlayColourProvider . Background6 ) ,
2022-06-07 11:42:19 +00:00
Width = 100 ,
2022-06-07 05:40:21 +00:00
RelativeSizeAxes = Axes . Y ,
Anchor = Anchor . TopCentre ,
Origin = Anchor . TopRight ,
} ,
2022-06-07 06:21:19 +00:00
explanatoryText = new OsuTextFlowContainer ( cp = > cp . Font = OsuFont . Default . With ( size : 20 ) )
{
2022-06-07 09:11:54 +00:00
Anchor = Anchor . BottomCentre ,
Origin = Anchor . BottomCentre ,
2022-06-07 06:21:19 +00:00
TextAnchor = Anchor . TopCentre ,
RelativeSizeAxes = Axes . X ,
AutoSizeAxes = Axes . Y ,
2022-06-07 14:34:19 +00:00
Text = @ "Welcome to the latency certifier!
2022-06-10 07:22:34 +00:00
Use the arrow keys , Z / X / F / J to control the display .
2022-06-07 11:28:42 +00:00
Use the Tab key to change focus .
2022-06-10 07:22:34 +00:00
Change display modes with Space .
2022-06-07 06:21:19 +00:00
Do whatever you need to try and perceive the difference in latency , then choose your best side .
",
} ,
2022-06-07 13:52:24 +00:00
resultsArea = new Container
{
RelativeSizeAxes = Axes . Both ,
} ,
2022-06-07 05:51:16 +00:00
statusText = new OsuTextFlowContainer ( cp = > cp . Font = OsuFont . Default . With ( size : 40 ) )
2022-06-07 05:40:21 +00:00
{
2022-06-07 09:11:54 +00:00
Anchor = Anchor . TopCentre ,
Origin = Anchor . TopCentre ,
2022-06-07 05:51:16 +00:00
TextAnchor = Anchor . TopCentre ,
2022-06-07 11:02:26 +00:00
Y = 150 ,
2022-06-07 05:51:16 +00:00
RelativeSizeAxes = Axes . X ,
AutoSizeAxes = Axes . Y ,
} ,
2022-06-07 05:40:21 +00:00
} ;
}
2022-06-07 10:43:33 +00:00
protected override bool OnMouseMove ( MouseMoveEvent e )
{
if ( lastPoll > 0 )
pollingMax = ( int ) Math . Max ( pollingMax , 1000 / ( Clock . CurrentTime - lastPoll ) ) ;
lastPoll = Clock . CurrentTime ;
return base . OnMouseMove ( e ) ;
}
2022-06-07 05:40:21 +00:00
public override void OnEntering ( ScreenTransitionEvent e )
{
base . OnEntering ( e ) ;
previousFrameSyncMode = config . Get < FrameSync > ( FrameworkSetting . FrameSync ) ;
2022-06-07 10:03:26 +00:00
previousActiveHz = host . UpdateThread . ActiveHz ;
2022-06-07 05:40:21 +00:00
config . SetValue ( FrameworkSetting . FrameSync , FrameSync . Unlimited ) ;
2022-06-07 10:03:26 +00:00
host . UpdateThread . ActiveHz = target_host_update_frames ;
2022-06-07 14:25:45 +00:00
host . AllowBenchmarkUnlimitedFrames = true ;
2022-06-07 05:40:21 +00:00
}
public override bool OnExiting ( ScreenExitEvent e )
{
2022-06-07 14:25:45 +00:00
host . AllowBenchmarkUnlimitedFrames = false ;
2022-06-07 05:40:21 +00:00
config . SetValue ( FrameworkSetting . FrameSync , previousFrameSyncMode ) ;
2022-06-07 10:03:26 +00:00
host . UpdateThread . ActiveHz = previousActiveHz ;
2022-06-07 05:40:21 +00:00
return base . OnExiting ( e ) ;
}
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
loadNextRound ( ) ;
}
2022-06-07 11:28:42 +00:00
protected override bool OnKeyDown ( KeyDownEvent e )
{
switch ( e . Key )
{
2022-06-10 07:22:34 +00:00
case Key . Space :
visualMode . Value = ( LatencyVisualMode ) ( ( ( int ) visualMode . Value + 1 ) % 3 ) ;
return true ;
2022-06-07 11:28:42 +00:00
case Key . Tab :
var firstArea = mainArea . FirstOrDefault ( a = > ! a . IsActiveArea . Value ) ;
if ( firstArea ! = null )
firstArea . IsActiveArea . Value = true ;
return true ;
}
return base . OnKeyDown ( e ) ;
}
2022-06-07 05:40:21 +00:00
private void showResults ( )
{
2022-06-07 05:51:16 +00:00
mainArea . Clear ( ) ;
2022-06-07 15:36:19 +00:00
var displayMode = host . Window ? . CurrentDisplayMode . Value ;
2022-06-07 10:03:26 +00:00
string exclusive = "unknown" ;
if ( host . Window is WindowsWindow windowsWindow )
exclusive = windowsWindow . FullscreenCapability . ToString ( ) ;
statusText . Clear ( ) ;
2022-06-10 06:19:10 +00:00
float successRate = ( float ) correctAtCurrentDifficulty / attemptsAtCurrentDifficulty ;
2022-06-07 13:52:24 +00:00
bool isPass = successRate = = 1 ;
2022-06-07 10:03:26 +00:00
2022-06-10 06:19:10 +00:00
statusText . AddParagraph ( $"You scored {correctAtCurrentDifficulty} out of {attemptsAtCurrentDifficulty} ({successRate:0%})!" , cp = > cp . Colour = isPass ? colours . Green : colours . Red ) ;
statusText . AddParagraph ( $"Level {DifficultyLevel} ({mapDifficultyToTargetFrameRate(DifficultyLevel):N0} Hz)" ,
2022-06-07 10:43:33 +00:00
cp = > cp . Font = OsuFont . Default . With ( size : 24 ) ) ;
2022-06-07 10:03:26 +00:00
2022-06-07 11:02:26 +00:00
statusText . AddParagraph ( string . Empty ) ;
statusText . AddParagraph ( string . Empty ) ;
statusText . AddIcon ( isPass ? FontAwesome . Regular . CheckCircle : FontAwesome . Regular . TimesCircle , cp = > cp . Colour = isPass ? colours . Green : colours . Red ) ;
statusText . AddParagraph ( string . Empty ) ;
2022-06-10 06:19:10 +00:00
if ( ! isPass & & DifficultyLevel > 1 )
2022-06-07 13:52:24 +00:00
{
2022-06-10 06:19:10 +00:00
statusText . AddParagraph ( "To complete certification, the difficulty level will now decrease until you can get 20 rounds correct in a row!" ,
2022-06-10 05:23:24 +00:00
cp = > cp . Font = OsuFont . Default . With ( size : 24 , weight : FontWeight . SemiBold ) ) ;
2022-06-07 13:52:24 +00:00
statusText . AddParagraph ( string . Empty ) ;
}
2022-06-10 05:38:04 +00:00
statusText . AddParagraph ( $"Polling: {pollingMax} Hz Monitor: {displayMode?.RefreshRate ?? 0:N0} Hz Exclusive: {exclusive}" ,
2022-06-10 05:23:24 +00:00
cp = > cp . Font = OsuFont . Default . With ( size : 15 , weight : FontWeight . SemiBold ) ) ;
2022-06-07 11:02:26 +00:00
2022-06-10 05:38:04 +00:00
statusText . AddParagraph ( $"Input: {host.InputThread.Clock.FramesPerSecond} Hz "
+ $"Update: {host.UpdateThread.Clock.FramesPerSecond} Hz "
+ $"Draw: {host.DrawThread.Clock.FramesPerSecond} Hz"
2022-06-09 10:26:24 +00:00
, cp = > cp . Font = OsuFont . Default . With ( size : 15 , weight : FontWeight . SemiBold ) ) ;
2022-06-07 10:03:26 +00:00
2022-06-10 06:19:10 +00:00
if ( isCertifying & & isPass )
2022-06-07 13:52:24 +00:00
{
2022-06-10 06:19:10 +00:00
showCertifiedScreen ( ) ;
2022-06-07 13:52:24 +00:00
return ;
}
2022-06-07 10:03:26 +00:00
string cannotIncreaseReason = string . Empty ;
2022-06-10 06:19:10 +00:00
if ( mapDifficultyToTargetFrameRate ( DifficultyLevel + 1 ) > target_host_update_frames )
2022-06-10 05:25:15 +00:00
cannotIncreaseReason = "You've reached the maximum level." ;
2022-06-10 06:19:10 +00:00
else if ( mapDifficultyToTargetFrameRate ( DifficultyLevel + 1 ) > Clock . FramesPerSecond )
2022-06-07 10:03:26 +00:00
cannotIncreaseReason = "Game is not running fast enough to test this level" ;
2022-06-07 05:51:16 +00:00
2022-06-10 06:19:10 +00:00
FillFlowContainer buttonFlow ;
resultsArea . Add ( buttonFlow = new FillFlowContainer
2022-06-07 05:40:21 +00:00
{
2022-06-07 11:02:26 +00:00
RelativeSizeAxes = Axes . X ,
AutoSizeAxes = Axes . Y ,
Anchor = Anchor . BottomLeft ,
Origin = Anchor . BottomLeft ,
2022-06-07 09:11:54 +00:00
Spacing = new Vector2 ( 20 ) ,
2022-06-07 11:02:26 +00:00
Padding = new MarginPadding ( 20 ) ,
2022-06-10 06:19:10 +00:00
} ) ;
if ( isPass )
{
buttonFlow . Add ( new ButtonWithKeyBind ( Key . Enter )
2022-06-07 05:40:21 +00:00
{
2022-06-10 06:19:10 +00:00
Text = "Continue to next level" ,
BackgroundColour = colours . Green ,
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
Action = ( ) = > changeDifficulty ( DifficultyLevel + 1 ) ,
Enabled = { Value = string . IsNullOrEmpty ( cannotIncreaseReason ) } ,
TooltipText = cannotIncreaseReason
} ) ;
}
else
{
if ( DifficultyLevel = = 1 )
{
buttonFlow . Add ( new ButtonWithKeyBind ( Key . Enter )
2022-06-07 09:11:54 +00:00
{
2022-06-10 06:19:10 +00:00
Text = "Retry" ,
TooltipText = "Are you even trying..?" ,
BackgroundColour = colours . Pink2 ,
2022-06-07 09:11:54 +00:00
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
2022-06-10 06:19:10 +00:00
Action = ( ) = >
{
isCertifying = false ;
changeDifficulty ( 1 ) ;
} ,
} ) ;
}
else
{
buttonFlow . Add ( new ButtonWithKeyBind ( Key . Enter )
2022-06-07 13:52:24 +00:00
{
2022-06-10 06:19:10 +00:00
Text = "Begin certification at last level" ,
BackgroundColour = colours . Yellow ,
2022-06-07 13:52:24 +00:00
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
Action = ( ) = >
{
2022-06-10 06:19:10 +00:00
isCertifying = true ;
changeDifficulty ( DifficultyLevel - 1 ) ;
2022-06-07 13:52:24 +00:00
} ,
2022-06-10 07:22:34 +00:00
TooltipText = isPass
? $"Chain {rounds_to_complete_certified} rounds to confirm your perception!"
: "You've reached your limits. Go to the previous level to complete certification!" ,
2022-06-10 06:19:10 +00:00
} ) ;
}
}
}
private void showCertifiedScreen ( )
{
Drawable background ;
Drawable certifiedText ;
resultsArea . AddRange ( new [ ]
{
background = new Box
{
Colour = overlayColourProvider . Background4 ,
RelativeSizeAxes = Axes . Both ,
} ,
( certifiedText = new OsuSpriteText
{
Alpha = 0 ,
Font = OsuFont . TorusAlternate . With ( size : 80 , weight : FontWeight . Bold ) ,
Text = "Certified!" ,
Blending = BlendingParameters . Additive ,
} ) . WithEffect ( new GlowEffect
{
Colour = overlayColourProvider . Colour1 ,
PadExtent = true
} ) . With ( e = >
{
e . Anchor = Anchor . Centre ;
e . Origin = Anchor . Centre ;
} ) ,
new OsuSpriteText
{
Text = $"You should use a frame limiter with update rate of {mapDifficultyToTargetFrameRate(DifficultyLevel + 1)} Hz (or fps) for best results!" ,
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
Font = OsuFont . Torus . With ( size : 24 , weight : FontWeight . SemiBold ) ,
Y = 80 ,
2022-06-07 05:40:21 +00:00
}
} ) ;
2022-06-10 06:19:10 +00:00
background . FadeInFromZero ( 1000 , Easing . OutQuint ) ;
certifiedText . FadeInFromZero ( 500 , Easing . InQuint ) ;
certifiedText
. ScaleTo ( 10 )
. ScaleTo ( 1 , 600 , Easing . InQuad )
. Then ( )
. ScaleTo ( 1.05f , 10000 , Easing . OutQuint ) ;
2022-06-07 05:40:21 +00:00
}
2022-06-07 14:10:08 +00:00
private void changeDifficulty ( int difficulty )
2022-06-07 09:11:54 +00:00
{
2022-06-07 14:10:08 +00:00
Debug . Assert ( difficulty > 0 ) ;
2022-06-07 09:11:54 +00:00
resultsArea . Clear ( ) ;
2022-06-10 05:23:24 +00:00
correctAtCurrentDifficulty = 0 ;
attemptsAtCurrentDifficulty = 0 ;
2022-06-07 10:43:33 +00:00
pollingMax = 0 ;
lastPoll = 0 ;
2022-06-07 09:11:54 +00:00
2022-06-10 06:19:10 +00:00
DifficultyLevel = difficulty ;
2022-06-07 14:10:08 +00:00
2022-06-07 09:11:54 +00:00
loadNextRound ( ) ;
}
2022-06-07 14:10:08 +00:00
private void loadNextRound ( )
{
2022-06-10 05:23:24 +00:00
attemptsAtCurrentDifficulty + + ;
2022-06-10 06:19:10 +00:00
statusText . Text = $"Level {DifficultyLevel}\nRound {attemptsAtCurrentDifficulty} of {totalRoundForNextResultsScreen}" ;
2022-06-07 14:10:08 +00:00
mainArea . Clear ( ) ;
int betterSide = RNG . Next ( 0 , 2 ) ;
mainArea . AddRange ( new [ ]
{
2022-06-10 06:19:10 +00:00
new LatencyArea ( Key . Number1 , betterSide = = 1 ? mapDifficultyToTargetFrameRate ( DifficultyLevel ) : ( int? ) null )
2022-06-07 14:10:08 +00:00
{
Width = 0.5f ,
2022-06-10 07:22:34 +00:00
VisualMode = { BindTarget = visualMode } ,
2022-06-07 14:10:08 +00:00
IsActiveArea = { Value = true } ,
ReportUserBest = ( ) = > recordResult ( betterSide = = 0 ) ,
} ,
2022-06-10 06:19:10 +00:00
new LatencyArea ( Key . Number2 , betterSide = = 0 ? mapDifficultyToTargetFrameRate ( DifficultyLevel ) : ( int? ) null )
2022-06-07 14:10:08 +00:00
{
Width = 0.5f ,
2022-06-10 07:22:34 +00:00
VisualMode = { BindTarget = visualMode } ,
2022-06-07 14:10:08 +00:00
Anchor = Anchor . TopRight ,
Origin = Anchor . TopRight ,
ReportUserBest = ( ) = > recordResult ( betterSide = = 1 )
}
} ) ;
foreach ( var area in mainArea )
{
area . IsActiveArea . BindValueChanged ( active = >
{
if ( active . NewValue )
mainArea . Children . First ( a = > a ! = area ) . IsActiveArea . Value = false ;
} ) ;
}
}
private void recordResult ( bool correct )
{
// Fading this out will improve the frame rate after the first round due to less text on screen.
explanatoryText . FadeOut ( 500 , Easing . OutQuint ) ;
if ( correct )
2022-06-10 05:23:24 +00:00
correctAtCurrentDifficulty + + ;
2022-06-07 14:10:08 +00:00
2022-06-10 05:23:24 +00:00
if ( attemptsAtCurrentDifficulty < totalRoundForNextResultsScreen )
2022-06-07 14:10:08 +00:00
loadNextRound ( ) ;
else
showResults ( ) ;
}
2022-06-07 10:03:26 +00:00
private static int mapDifficultyToTargetFrameRate ( int difficulty )
{
switch ( difficulty )
{
case 1 :
return 15 ;
case 2 :
return 30 ;
case 3 :
return 45 ;
case 4 :
return 60 ;
case 5 :
return 120 ;
case 6 :
return 240 ;
case 7 :
return 480 ;
case 8 :
return 720 ;
case 9 :
return 960 ;
default :
return 1000 + ( ( difficulty - 10 ) * 500 ) ;
}
}
2022-06-07 05:40:21 +00:00
}
}