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 ;
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
{
public class LatencyComparerScreen : OsuScreen
{
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-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-07 09:11:54 +00:00
private int round ;
private int correctCount ;
private int targetRoundCount = rounds_to_complete ;
2022-06-07 13:52:24 +00:00
private int difficultyLevel = 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 05:40:21 +00:00
public LatencyComparerScreen ( )
{
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 ,
Text = @ "Welcome to the latency comparer!
2022-06-07 11:03:49 +00:00
Use the arrow keys , Z / X / J / K to move the square .
2022-06-07 06:21:19 +00:00
You can click the targets but you don ' t have to .
2022-06-07 11:28:42 +00:00
Use the Tab key to change focus .
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 )
{
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 10:03:26 +00:00
var displayMode = host . Window . CurrentDisplayMode . Value ;
string exclusive = "unknown" ;
if ( host . Window is WindowsWindow windowsWindow )
exclusive = windowsWindow . FullscreenCapability . ToString ( ) ;
statusText . Clear ( ) ;
float successRate = ( float ) correctCount / targetRoundCount ;
2022-06-07 13:52:24 +00:00
bool isPass = successRate = = 1 ;
2022-06-07 10:03:26 +00:00
2022-06-07 11:02:26 +00:00
statusText . AddParagraph ( $"You scored {correctCount} out of {targetRoundCount} ({successRate:0%})!" , cp = > cp . Colour = isPass ? colours . Green : colours . Red ) ;
2022-06-07 13:52:24 +00:00
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-07 13:52:24 +00:00
if ( ! isPass & & difficultyLevel > 1 )
{
statusText . AddParagraph ( "To complete certification, decrease the difficulty level until you can get 20 tests correct in a row!" , cp = > cp . Font = OsuFont . Default . With ( size : 24 ) ) ;
statusText . AddParagraph ( string . Empty ) ;
}
2022-06-07 11:02:26 +00:00
statusText . AddParagraph ( $"Polling: {pollingMax} hz Monitor: {displayMode.RefreshRate:N0} hz Exclusive: {exclusive}" , cp = > cp . Font = OsuFont . Default . With ( size : 15 ) ) ;
statusText . AddParagraph ( $"Input: {host.InputThread.Clock.FramesPerSecond} hz "
+ $"Update: {host.UpdateThread.Clock.FramesPerSecond} hz "
+ $"Draw: {host.DrawThread.Clock.FramesPerSecond} hz"
, cp = > cp . Font = OsuFont . Default . With ( size : 15 ) ) ;
2022-06-07 10:03:26 +00:00
2022-06-07 13:52:24 +00:00
int certificationRemaining = ! isPass ? rounds_to_complete_certified : rounds_to_complete_certified - correctCount ;
if ( isPass & & certificationRemaining < = 0 )
{
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 ,
} ) . With ( e = >
{
e . Anchor = Anchor . Centre ;
e . Origin = Anchor . Centre ;
} )
} ) ;
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 ) ;
return ;
}
2022-06-07 10:03:26 +00:00
string cannotIncreaseReason = string . Empty ;
if ( ! isPass )
2022-06-07 13:52:24 +00:00
cannotIncreaseReason = "You didn't get a perfect score." ;
else if ( mapDifficultyToTargetFrameRate ( difficultyLevel + 1 ) > target_host_update_frames )
2022-06-07 10:03:26 +00:00
cannotIncreaseReason = "You've reached the limits of this comparison mode." ;
2022-06-07 13:52:24 +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-07 09:11:54 +00:00
resultsArea . Add ( 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-07 05:40:21 +00:00
Children = new Drawable [ ]
{
2022-06-07 14:10:08 +00:00
new ButtonWithKeyBind ( Key . Enter )
2022-06-07 09:11:54 +00:00
{
2022-06-07 13:52:24 +00:00
Text = "Continue to next level" ,
2022-06-07 09:11:54 +00:00
BackgroundColour = colours . Red2 ,
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
2022-06-07 13:52:24 +00:00
Action = ( ) = > changeDifficulty ( difficultyLevel + 1 ) ,
2022-06-07 10:03:26 +00:00
Enabled = { Value = string . IsNullOrEmpty ( cannotIncreaseReason ) } ,
TooltipText = cannotIncreaseReason
2022-06-07 09:11:54 +00:00
} ,
2022-06-07 14:10:08 +00:00
new ButtonWithKeyBind ( Key . D )
2022-06-07 09:11:54 +00:00
{
2022-06-07 13:52:24 +00:00
Text = difficultyLevel = = 1 ? "Retry" : "Return to last level" ,
2022-06-07 09:11:54 +00:00
BackgroundColour = colours . Green ,
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
2022-06-07 13:52:24 +00:00
Action = ( ) = > changeDifficulty ( Math . Max ( difficultyLevel - 1 , 1 ) ) ,
} ,
2022-06-07 14:10:08 +00:00
new ButtonWithKeyBind ( Key . C )
2022-06-07 13:52:24 +00:00
{
Text = $"Continue towards certification at this level ({certificationRemaining} more)" ,
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
Action = ( ) = >
{
resultsArea . Clear ( ) ;
targetRoundCount + = rounds_to_complete ;
loadNextRound ( ) ;
} ,
TooltipText = isPass ? $"Chain {rounds_to_complete_certified} to confirm your perception!" : "You've reached your limits. Go to the previous level to complete certification!" ,
Enabled = { Value = isPass } ,
} ,
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 ( ) ;
correctCount = 0 ;
round = 0 ;
2022-06-07 10:43:33 +00:00
pollingMax = 0 ;
lastPoll = 0 ;
2022-06-07 09:11:54 +00:00
targetRoundCount = rounds_to_complete ;
2022-06-07 14:10:08 +00:00
difficultyLevel = difficulty ;
2022-06-07 09:11:54 +00:00
loadNextRound ( ) ;
}
2022-06-07 14:10:08 +00:00
private void loadNextRound ( )
{
round + + ;
statusText . Text = $"Level {difficultyLevel}\nRound {round} of {targetRoundCount}" ;
mainArea . Clear ( ) ;
int betterSide = RNG . Next ( 0 , 2 ) ;
mainArea . AddRange ( new [ ]
{
new LatencyArea ( Key . Number1 , betterSide = = 1 ? mapDifficultyToTargetFrameRate ( difficultyLevel ) : 0 )
{
Width = 0.5f ,
IsActiveArea = { Value = true } ,
ReportUserBest = ( ) = > recordResult ( betterSide = = 0 ) ,
} ,
new LatencyArea ( Key . Number2 , betterSide = = 0 ? mapDifficultyToTargetFrameRate ( difficultyLevel ) : 0 )
{
Width = 0.5f ,
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 )
correctCount + + ;
if ( round < targetRoundCount )
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
}
}