2019-05-15 16:36:29 +08:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2019-01-24 16:43:03 +08:00
// See the LICENCE file in the repository root for full licence text.
2018-04-13 17:19:50 +08:00
using System ;
using System.Collections.Generic ;
2019-02-28 16:17:51 +08:00
using System.Diagnostics ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Configuration ;
using osu.Framework.Screens ;
using osu.Game.Configuration ;
using osu.Framework.Graphics ;
using osu.Framework.Graphics.Containers ;
using osu.Game.Overlays ;
using osu.Framework.Logging ;
using osu.Framework.Allocation ;
using osu.Game.Overlays.Toolbar ;
using osu.Game.Screens ;
using osu.Game.Screens.Menu ;
using System.Linq ;
using System.Threading ;
using System.Threading.Tasks ;
using osu.Framework.Audio ;
2019-02-21 18:04:31 +08:00
using osu.Framework.Bindables ;
2018-07-13 19:32:22 +08:00
using osu.Framework.Extensions.IEnumerableExtensions ;
2019-03-27 18:29:27 +08:00
using osu.Framework.Graphics.Sprites ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Input ;
using osu.Framework.Input.Bindings ;
using osu.Framework.Platform ;
using osu.Framework.Threading ;
2018-07-11 00:32:10 +08:00
using osu.Game.Beatmaps ;
2018-04-13 17:19:50 +08:00
using osu.Game.Graphics ;
2019-01-04 12:29:37 +08:00
using osu.Game.Graphics.Containers ;
2019-06-25 15:55:49 +08:00
using osu.Game.Graphics.UserInterface ;
2018-11-20 01:48:59 +08:00
using osu.Game.Input ;
2018-04-13 17:19:50 +08:00
using osu.Game.Overlays.Notifications ;
using osu.Game.Screens.Play ;
using osu.Game.Input.Bindings ;
2018-09-14 11:06:04 +08:00
using osu.Game.Online.Chat ;
2018-04-13 17:19:50 +08:00
using osu.Game.Skinning ;
2018-11-20 15:51:59 +08:00
using osuTK.Graphics ;
2018-04-13 17:19:50 +08:00
using osu.Game.Overlays.Volume ;
2018-11-28 15:12:57 +08:00
using osu.Game.Scoring ;
2018-07-11 00:32:10 +08:00
using osu.Game.Screens.Select ;
2018-08-03 18:25:55 +08:00
using osu.Game.Utils ;
using LogLevel = osu . Framework . Logging . LogLevel ;
2018-04-13 17:19:50 +08:00
namespace osu.Game
{
/// <summary>
/// The full osu! experience. Builds on top of <see cref="OsuGameBase"/> to add menus and binding logic
/// for initial components that are generally retrieved via DI.
/// </summary>
public class OsuGame : OsuGameBase , IKeyBindingHandler < GlobalAction >
{
public Toolbar Toolbar ;
2018-11-23 10:00:17 +08:00
private ChatOverlay chatOverlay ;
private ChannelManager channelManager ;
2018-04-13 17:19:50 +08:00
private NotificationOverlay notifications ;
private DirectOverlay direct ;
private SocialOverlay social ;
private UserProfileOverlay userProfile ;
private BeatmapSetOverlay beatmapSetOverlay ;
2018-10-02 09:12:07 +08:00
[Cached]
private readonly ScreenshotManager screenshotManager = new ScreenshotManager ( ) ;
2018-04-13 20:13:09 +08:00
2018-08-03 18:25:55 +08:00
protected RavenLogger RavenLogger ;
2018-04-13 17:19:50 +08:00
public virtual Storage GetStorageForStableInstall ( ) = > null ;
public float ToolbarOffset = > Toolbar . Position . Y + Toolbar . DrawHeight ;
2018-11-20 01:48:59 +08:00
private IdleTracker idleTracker ;
2018-06-06 14:49:27 +08:00
public readonly Bindable < OverlayActivation > OverlayActivationMode = new Bindable < OverlayActivation > ( ) ;
2018-04-13 17:19:50 +08:00
2019-03-12 16:33:16 +08:00
private OsuScreenStack screenStack ;
2018-04-13 17:19:50 +08:00
private VolumeOverlay volume ;
2019-01-23 19:52:00 +08:00
private OsuLogo osuLogo ;
2019-06-25 15:55:49 +08:00
private BackButton backButton ;
2019-01-23 19:52:00 +08:00
private MainMenu menuScreen ;
private Intro introScreen ;
2018-04-13 17:19:50 +08:00
private Bindable < int > configRuleset ;
private Bindable < int > configSkin ;
private readonly string [ ] args ;
2019-05-14 09:45:05 +08:00
private SettingsPanel settings ;
2018-04-13 17:19:50 +08:00
2018-06-06 15:17:51 +08:00
private readonly List < OverlayContainer > overlays = new List < OverlayContainer > ( ) ;
2019-05-14 17:34:25 +08:00
private readonly List < OverlayContainer > toolbarElements = new List < OverlayContainer > ( ) ;
2019-03-01 11:20:31 +08:00
private readonly List < OverlayContainer > visibleBlockingOverlays = new List < OverlayContainer > ( ) ;
2018-04-13 17:19:50 +08:00
public OsuGame ( string [ ] args = null )
{
this . args = args ;
2018-06-21 13:43:38 +08:00
forwardLoggedErrorsToNotifications ( ) ;
2018-08-03 18:25:55 +08:00
2018-09-13 01:34:52 +08:00
RavenLogger = new RavenLogger ( this ) ;
2018-04-13 17:19:50 +08:00
}
2019-03-01 11:20:31 +08:00
private void updateBlockingOverlayFade ( ) = >
screenContainer . FadeColour ( visibleBlockingOverlays . Any ( ) ? OsuColour . Gray ( 0.5f ) : Color4 . White , 500 , Easing . OutQuint ) ;
public void AddBlockingOverlay ( OverlayContainer overlay )
{
if ( ! visibleBlockingOverlays . Contains ( overlay ) )
visibleBlockingOverlays . Add ( overlay ) ;
updateBlockingOverlayFade ( ) ;
}
public void RemoveBlockingOverlay ( OverlayContainer overlay )
{
2019-03-01 12:29:02 +08:00
visibleBlockingOverlays . Remove ( overlay ) ;
2019-03-01 11:20:31 +08:00
updateBlockingOverlayFade ( ) ;
}
2018-06-06 15:17:51 +08:00
/// <summary>
/// Close all game-wide overlays.
/// </summary>
2019-05-12 21:34:36 +08:00
/// <param name="hideToolbarElements">Whether the toolbar (and accompanying controls) should also be hidden.</param>
public void CloseAllOverlays ( bool hideToolbarElements = true )
2018-06-06 15:17:51 +08:00
{
2018-08-30 06:04:51 +08:00
foreach ( var overlay in overlays )
2019-06-11 13:28:52 +08:00
overlay . Hide ( ) ;
2019-05-12 21:34:36 +08:00
if ( hideToolbarElements )
{
2019-05-14 17:34:25 +08:00
foreach ( var overlay in toolbarElements )
2019-06-11 13:28:52 +08:00
overlay . Hide ( ) ;
2019-05-12 21:34:36 +08:00
}
2018-06-06 15:17:51 +08:00
}
2018-04-13 17:19:50 +08:00
private DependencyContainer dependencies ;
2018-07-11 16:07:14 +08:00
protected override IReadOnlyDependencyContainer CreateChildDependencies ( IReadOnlyDependencyContainer parent ) = >
dependencies = new DependencyContainer ( base . CreateChildDependencies ( parent ) ) ;
2018-04-13 17:19:50 +08:00
[BackgroundDependencyLoader]
private void load ( FrameworkConfigManager frameworkConfig )
{
this . frameworkConfig = frameworkConfig ;
if ( ! Host . IsPrimaryInstance )
{
Logger . Log ( @"osu! does not support multiple running instances." , LoggingTarget . Runtime , LogLevel . Error ) ;
Environment . Exit ( 0 ) ;
}
if ( args ? . Length > 0 )
{
2018-08-16 21:01:04 +08:00
var paths = args . Where ( a = > ! a . StartsWith ( @"-" ) ) . ToArray ( ) ;
if ( paths . Length > 0 )
Task . Run ( ( ) = > Import ( paths ) ) ;
2018-04-13 17:19:50 +08:00
}
dependencies . CacheAs ( this ) ;
2018-08-03 18:25:55 +08:00
dependencies . Cache ( RavenLogger ) ;
2019-01-31 18:22:29 +08:00
dependencies . Cache ( osuLogo = new OsuLogo { Alpha = 0 } ) ;
2019-01-23 19:52:00 +08:00
2018-04-13 17:19:50 +08:00
// bind config int to database RulesetInfo
configRuleset = LocalConfig . GetBindable < int > ( OsuSetting . Ruleset ) ;
2019-05-15 12:00:11 +08:00
Ruleset . Value = RulesetStore . GetRuleset ( configRuleset . Value ) ? ? RulesetStore . AvailableRulesets . First ( ) ;
Ruleset . ValueChanged + = r = > configRuleset . Value = r . NewValue . ID ? ? 0 ;
2018-04-13 17:19:50 +08:00
// bind config int to database SkinInfo
configSkin = LocalConfig . GetBindable < int > ( OsuSetting . Skin ) ;
2019-02-22 16:51:39 +08:00
SkinManager . CurrentSkinInfo . ValueChanged + = skin = > configSkin . Value = skin . NewValue . ID ;
configSkin . ValueChanged + = skinId = > SkinManager . CurrentSkinInfo . Value = SkinManager . Query ( s = > s . ID = = skinId . NewValue ) ? ? SkinInfo . Default ;
2018-04-13 17:19:50 +08:00
configSkin . TriggerChange ( ) ;
2019-02-22 19:13:38 +08:00
IsActive . BindValueChanged ( active = > updateActiveState ( active . NewValue ) , true ) ;
2019-06-18 00:32:52 +08:00
Audio . AddAdjustment ( AdjustableProperty . Volume , inactiveVolumeFade ) ;
2019-06-21 20:09:12 +08:00
2019-06-20 22:40:25 +08:00
Beatmap . BindValueChanged ( beatmapChanged , true ) ;
2018-04-13 17:19:50 +08:00
}
2018-11-02 04:52:07 +08:00
private ExternalLinkOpener externalLinkOpener ;
2019-01-04 12:29:37 +08:00
2018-12-06 11:17:08 +08:00
public void OpenUrlExternally ( string url )
{
if ( url . StartsWith ( "/" ) )
url = $"{API.Endpoint}{url}" ;
externalLinkOpener . OpenUrlExternally ( url ) ;
}
2018-11-02 04:52:07 +08:00
2018-04-13 17:19:50 +08:00
/// <summary>
/// Show a beatmap set as an overlay.
/// </summary>
/// <param name="setId">The set to display.</param>
2018-04-18 14:58:45 +08:00
public void ShowBeatmapSet ( int setId ) = > beatmapSetOverlay . FetchAndShowBeatmapSet ( setId ) ;
2018-04-13 17:19:50 +08:00
2019-02-25 11:58:58 +08:00
/// <summary>
/// Show a user's profile as an overlay.
/// </summary>
/// <param name="userId">The user to display.</param>
public void ShowUser ( long userId ) = > userProfile . ShowUser ( userId ) ;
/// <summary>
/// Show a beatmap's set as an overlay, displaying the given beatmap.
/// </summary>
/// <param name="beatmapId">The beatmap to show.</param>
public void ShowBeatmap ( int beatmapId ) = > beatmapSetOverlay . FetchAndShowBeatmap ( beatmapId ) ;
2018-07-11 00:32:10 +08:00
/// <summary>
2019-02-26 11:28:49 +08:00
/// Present a beatmap at song select immediately.
2019-02-25 17:24:06 +08:00
/// The user should have already requested this interactively.
2018-07-11 00:32:10 +08:00
/// </summary>
/// <param name="beatmap">The beatmap to select.</param>
public void PresentBeatmap ( BeatmapSetInfo beatmap )
{
2019-02-25 11:58:58 +08:00
var databasedSet = beatmap . OnlineBeatmapSetID ! = null
? BeatmapManager . QueryBeatmapSet ( s = > s . OnlineBeatmapSetID = = beatmap . OnlineBeatmapSetID )
: BeatmapManager . QueryBeatmapSet ( s = > s . Hash = = beatmap . Hash ) ;
if ( databasedSet = = null )
2019-01-23 19:52:00 +08:00
{
2019-02-25 11:58:58 +08:00
Logger . Log ( "The requested beatmap could not be loaded." , LoggingTarget . Information ) ;
2019-01-23 19:52:00 +08:00
return ;
}
2019-02-25 17:24:06 +08:00
performFromMainMenu ( ( ) = >
2018-07-13 20:08:41 +08:00
{
2019-02-25 17:24:06 +08:00
// we might already be at song select, so a check is required before performing the load to solo.
if ( menuScreen . IsCurrentScreen ( ) )
menuScreen . LoadToSolo ( ) ;
2019-02-24 11:08:27 +08:00
2019-04-02 23:57:31 +08:00
// we might even already be at the song
2019-04-03 21:49:33 +08:00
if ( Beatmap . Value . BeatmapSetInfo . Hash = = databasedSet . Hash )
2019-04-02 23:57:31 +08:00
{
return ;
}
2019-02-25 11:58:58 +08:00
// Use first beatmap available for current ruleset, else switch ruleset.
2019-05-15 12:00:11 +08:00
var first = databasedSet . Beatmaps . Find ( b = > b . Ruleset = = Ruleset . Value ) ? ? databasedSet . Beatmaps . First ( ) ;
2019-02-24 11:08:27 +08:00
2019-05-15 12:00:11 +08:00
Ruleset . Value = first . Ruleset ;
2019-02-25 11:58:58 +08:00
Beatmap . Value = BeatmapManager . GetWorkingBeatmap ( first ) ;
2019-02-25 17:24:06 +08:00
} , $"load {beatmap}" , bypassScreenAllowChecks : true , targetScreen : typeof ( PlaySongSelect ) ) ;
2018-07-11 00:32:10 +08:00
}
2018-04-13 17:19:50 +08:00
/// <summary>
2019-02-25 17:24:06 +08:00
/// Present a score's replay immediately.
/// The user should have already requested this interactively.
2018-04-13 17:19:50 +08:00
/// </summary>
2019-02-25 11:58:58 +08:00
public void PresentScore ( ScoreInfo score )
2018-04-13 17:19:50 +08:00
{
2019-06-29 18:40:16 +08:00
// The given ScoreInfo may have missing properties if it was retrieved from online data. Re-retrieve it from the database
// to ensure all the required data for presenting a replay are present.
2019-06-29 18:38:48 +08:00
var databasedScoreInfo = ScoreManager . Query ( s = > s . OnlineScoreID = = score . OnlineScoreID ) ;
var databasedScore = ScoreManager . GetScore ( databasedScoreInfo ) ;
2019-04-01 11:16:05 +08:00
2018-11-30 17:31:54 +08:00
if ( databasedScore . Replay = = null )
2018-11-29 12:06:48 +08:00
{
Logger . Log ( "The loaded score has no replay data." , LoggingTarget . Information ) ;
return ;
}
2018-11-28 19:41:48 +08:00
2018-11-30 17:31:54 +08:00
var databasedBeatmap = BeatmapManager . QueryBeatmap ( b = > b . ID = = databasedScoreInfo . Beatmap . ID ) ;
2019-04-01 11:16:05 +08:00
2018-11-30 17:31:54 +08:00
if ( databasedBeatmap = = null )
{
Logger . Log ( "Tried to load a score for a beatmap we don't have!" , LoggingTarget . Information ) ;
return ;
}
2019-02-25 11:58:58 +08:00
performFromMainMenu ( ( ) = >
{
2019-05-15 12:00:11 +08:00
Ruleset . Value = databasedScoreInfo . Ruleset ;
2019-02-25 11:58:58 +08:00
Beatmap . Value = BeatmapManager . GetWorkingBeatmap ( databasedBeatmap ) ;
2019-05-15 12:00:11 +08:00
Mods . Value = databasedScoreInfo . Mods ;
2019-02-25 11:58:58 +08:00
menuScreen . Push ( new PlayerLoader ( ( ) = > new ReplayPlayer ( databasedScore ) ) ) ;
2019-02-25 17:42:08 +08:00
} , $"watch {databasedScoreInfo}" , bypassScreenAllowChecks : true ) ;
2019-02-25 11:58:58 +08:00
}
2019-06-20 22:40:25 +08:00
#region Beatmap jukebox progression
private void beatmapChanged ( ValueChangedEvent < WorkingBeatmap > beatmap )
{
var nextBeatmap = beatmap . NewValue ;
if ( nextBeatmap ? . Track ! = null )
nextBeatmap . Track . Completed + = currentTrackCompleted ;
2019-06-24 16:10:50 +08:00
2019-07-02 21:25:03 +08:00
beatmap . OldValue ? . Dispose ( ) ;
2019-06-24 16:10:50 +08:00
nextBeatmap ? . LoadBeatmapAsync ( ) ;
2019-06-20 22:40:25 +08:00
}
private void currentTrackCompleted ( )
{
if ( ! Beatmap . Value . Track . Looping & & ! Beatmap . Disabled )
musicController . NextTrack ( ) ;
}
#endregion
2019-02-25 11:58:58 +08:00
private ScheduledDelegate performFromMainMenuTask ;
/// <summary>
/// Perform an action only after returning to the main menu.
/// Eagerly tries to exit the current screen until it succeeds.
/// </summary>
/// <param name="action">The action to perform once we are in the correct state.</param>
/// <param name="taskName">The task name to display in a notification (if we can't immediately reach the main menu state).</param>
2019-02-25 13:01:51 +08:00
/// <param name="targetScreen">An optional target screen type. If this screen is already current we can immediately perform the action without returning to the menu.</param>
/// <param name="bypassScreenAllowChecks">Whether checking <see cref="IOsuScreen.AllowExternalScreenChange"/> should be bypassed.</param>
private void performFromMainMenu ( Action action , string taskName , Type targetScreen = null , bool bypassScreenAllowChecks = false )
2019-02-25 11:58:58 +08:00
{
2019-02-25 13:01:51 +08:00
performFromMainMenuTask ? . Cancel ( ) ;
// if the current screen does not allow screen changing, give the user an option to try again later.
if ( ! bypassScreenAllowChecks & & ( screenStack . CurrentScreen as IOsuScreen ) ? . AllowExternalScreenChange = = false )
2018-04-13 17:19:50 +08:00
{
2018-11-29 16:18:59 +08:00
notifications . Post ( new SimpleNotification
{
2019-02-25 11:58:58 +08:00
Text = $"Click here to {taskName}" ,
2018-11-29 16:18:59 +08:00
Activated = ( ) = >
{
2019-02-25 13:01:51 +08:00
performFromMainMenu ( action , taskName , targetScreen , true ) ;
2018-11-29 16:18:59 +08:00
return true ;
}
} ) ;
2018-04-13 17:19:50 +08:00
return ;
}
2019-02-25 11:58:58 +08:00
CloseAllOverlays ( false ) ;
2018-11-29 16:18:59 +08:00
2019-02-25 13:01:51 +08:00
// we may already be at the target screen type.
if ( targetScreen ! = null & & screenStack . CurrentScreen ? . GetType ( ) = = targetScreen )
2019-02-25 11:58:58 +08:00
{
2019-02-25 13:01:51 +08:00
action ( ) ;
2019-02-25 11:58:58 +08:00
return ;
2018-11-29 16:18:59 +08:00
}
2019-02-25 11:58:58 +08:00
2019-02-25 13:01:51 +08:00
// all conditions have been met to continue with the action.
if ( menuScreen ? . IsCurrentScreen ( ) = = true & & ! Beatmap . Disabled )
{
action ( ) ;
return ;
}
// menuScreen may not be initialised yet (null check required).
menuScreen ? . MakeCurrent ( ) ;
performFromMainMenuTask = Schedule ( ( ) = > performFromMainMenu ( action , taskName ) ) ;
2018-04-13 17:19:50 +08:00
}
2018-08-03 18:25:55 +08:00
protected override void Dispose ( bool isDisposing )
{
base . Dispose ( isDisposing ) ;
RavenLogger . Dispose ( ) ;
}
2018-04-13 17:19:50 +08:00
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
2019-02-15 15:55:39 +08:00
// The next time this is updated is in UpdateAfterChildren, which occurs too late and results
// in the cursor being shown for a few frames during the intro.
// This prevents the cursor from showing until we have a screen with CursorVisible = true
MenuCursorContainer . CanShowCursor = menuScreen ? . CursorVisible ? ? false ;
2018-08-31 17:28:53 +08:00
// todo: all archive managers should be able to be looped here.
2018-04-13 17:19:50 +08:00
SkinManager . PostNotification = n = > notifications ? . Post ( n ) ;
2018-08-31 17:28:53 +08:00
SkinManager . GetStableStorage = GetStorageForStableInstall ;
2018-04-13 17:19:50 +08:00
2018-08-31 17:28:53 +08:00
BeatmapManager . PostNotification = n = > notifications ? . Post ( n ) ;
2018-04-13 17:19:50 +08:00
BeatmapManager . GetStableStorage = GetStorageForStableInstall ;
2019-02-25 17:24:06 +08:00
BeatmapManager . PresentImport = items = > PresentBeatmap ( items . First ( ) ) ;
2018-08-31 17:28:53 +08:00
2019-02-25 17:24:06 +08:00
ScoreManager . PostNotification = n = > notifications ? . Post ( n ) ;
ScoreManager . PresentImport = items = > PresentScore ( items . First ( ) ) ;
2018-04-13 17:19:50 +08:00
2019-01-23 19:52:00 +08:00
Container logoContainer ;
2019-05-13 16:10:25 +08:00
dependencies . CacheAs ( idleTracker = new GameIdleTracker ( 6000 ) ) ;
2018-04-13 17:19:50 +08:00
AddRange ( new Drawable [ ]
{
new VolumeControlReceptor
{
RelativeSizeAxes = Axes . Both ,
2018-06-27 17:43:29 +08:00
ActionRequested = action = > volume . Adjust ( action ) ,
2018-07-05 15:50:04 +08:00
ScrollActionRequested = ( action , amount , isPrecise ) = > volume . Adjust ( action , amount , isPrecise ) ,
2018-04-13 17:19:50 +08:00
} ,
2019-01-04 12:29:37 +08:00
screenContainer = new ScalingContainer ( ScalingMode . ExcludeOverlays )
{
RelativeSizeAxes = Axes . Both ,
2019-01-31 17:25:25 +08:00
Children = new Drawable [ ]
{
2019-03-24 15:21:43 +08:00
screenStack = new OsuScreenStack { RelativeSizeAxes = Axes . Both } ,
2019-06-25 16:16:38 +08:00
backButton = new BackButton
{
Anchor = Anchor . BottomLeft ,
Origin = Anchor . BottomLeft ,
2019-06-25 16:17:29 +08:00
Action = ( ) = >
{
if ( ( screenStack . CurrentScreen as IOsuScreen ) ? . AllowBackButton = = true )
screenStack . Exit ( ) ;
}
2019-06-25 16:16:38 +08:00
} ,
2019-01-31 17:25:25 +08:00
logoContainer = new Container { RelativeSizeAxes = Axes . Both } ,
}
2019-01-04 12:29:37 +08:00
} ,
2019-03-20 11:51:43 +08:00
overlayContent = new Container { RelativeSizeAxes = Axes . Both } ,
2019-04-05 15:00:21 +08:00
rightFloatingOverlayContent = new Container { RelativeSizeAxes = Axes . Both } ,
leftFloatingOverlayContent = new Container { RelativeSizeAxes = Axes . Both } ,
2019-03-22 02:16:10 +08:00
topMostOverlayContent = new Container { RelativeSizeAxes = Axes . Both } ,
2019-05-13 16:10:25 +08:00
idleTracker
2018-04-13 17:19:50 +08:00
} ) ;
2019-01-23 19:52:00 +08:00
screenStack . ScreenPushed + = screenPushed ;
screenStack . ScreenExited + = screenExited ;
2019-03-12 15:03:25 +08:00
loadComponentSingleFile ( osuLogo , logo = >
2018-04-13 17:19:50 +08:00
{
2019-03-12 15:03:25 +08:00
logoContainer . Add ( logo ) ;
2019-01-23 19:52:00 +08:00
2019-03-24 15:21:43 +08:00
// Loader has to be created after the logo has finished loading as Loader performs logo transformations on entering.
2019-03-12 15:03:25 +08:00
screenStack . Push ( new Loader
{
RelativeSizeAxes = Axes . Both
} ) ;
} ) ;
2018-04-13 17:19:50 +08:00
loadComponentSingleFile ( Toolbar = new Toolbar
{
OnHome = delegate
{
2018-06-06 15:17:51 +08:00
CloseAllOverlays ( false ) ;
2019-01-23 19:52:00 +08:00
menuScreen ? . MakeCurrent ( ) ;
2018-04-13 17:19:50 +08:00
} ,
2019-05-14 17:34:25 +08:00
} , d = >
{
topMostOverlayContent . Add ( d ) ;
toolbarElements . Add ( d ) ;
} ) ;
2018-04-13 17:19:50 +08:00
2019-04-05 15:05:42 +08:00
loadComponentSingleFile ( volume = new VolumeOverlay ( ) , leftFloatingOverlayContent . Add ) ;
2019-05-13 16:10:25 +08:00
loadComponentSingleFile ( new OnScreenDisplay ( ) , Add , true ) ;
2018-04-13 20:13:09 +08:00
2019-04-06 23:40:48 +08:00
loadComponentSingleFile ( notifications = new NotificationOverlay
2019-03-29 13:53:40 +08:00
{
GetToolbarHeight = ( ) = > ToolbarOffset ,
Anchor = Anchor . TopRight ,
Origin = Anchor . TopRight ,
2019-05-13 16:10:25 +08:00
} , rightFloatingOverlayContent . Add , true ) ;
2019-03-29 13:53:40 +08:00
2018-04-13 20:13:09 +08:00
loadComponentSingleFile ( screenshotManager , Add ) ;
2018-04-13 17:19:50 +08:00
//overlay elements
2019-05-13 16:10:25 +08:00
loadComponentSingleFile ( direct = new DirectOverlay ( ) , overlayContent . Add , true ) ;
loadComponentSingleFile ( social = new SocialOverlay ( ) , overlayContent . Add , true ) ;
loadComponentSingleFile ( channelManager = new ChannelManager ( ) , AddInternal , true ) ;
loadComponentSingleFile ( chatOverlay = new ChatOverlay ( ) , overlayContent . Add , true ) ;
2019-05-14 09:45:05 +08:00
loadComponentSingleFile ( settings = new SettingsOverlay { GetToolbarHeight = ( ) = > ToolbarOffset } , leftFloatingOverlayContent . Add , true ) ;
2019-05-31 12:23:50 +08:00
var changelogOverlay = loadComponentSingleFile ( new ChangelogOverlay ( ) , overlayContent . Add , true ) ;
2019-05-13 16:10:25 +08:00
loadComponentSingleFile ( userProfile = new UserProfileOverlay ( ) , overlayContent . Add , true ) ;
loadComponentSingleFile ( beatmapSetOverlay = new BeatmapSetOverlay ( ) , overlayContent . Add , true ) ;
loadComponentSingleFile ( new LoginOverlay
2018-04-13 17:19:50 +08:00
{
2019-03-20 12:30:24 +08:00
GetToolbarHeight = ( ) = > ToolbarOffset ,
2018-04-13 17:19:50 +08:00
Anchor = Anchor . TopRight ,
Origin = Anchor . TopRight ,
2019-05-13 16:10:25 +08:00
} , rightFloatingOverlayContent . Add , true ) ;
2018-04-13 17:19:50 +08:00
2019-06-20 22:40:25 +08:00
loadComponentSingleFile ( musicController = new MusicController
2018-04-13 17:19:50 +08:00
{
2019-03-28 11:27:26 +08:00
GetToolbarHeight = ( ) = > ToolbarOffset ,
2018-04-13 17:19:50 +08:00
Anchor = Anchor . TopRight ,
Origin = Anchor . TopRight ,
2019-05-14 17:34:25 +08:00
} , d = >
{
rightFloatingOverlayContent . Add ( d ) ;
toolbarElements . Add ( d ) ;
} , true ) ;
2018-04-13 17:19:50 +08:00
2019-05-13 16:10:25 +08:00
loadComponentSingleFile ( new AccountCreationOverlay ( ) , topMostOverlayContent . Add , true ) ;
loadComponentSingleFile ( new DialogOverlay ( ) , topMostOverlayContent . Add , true ) ;
2019-03-22 02:16:10 +08:00
loadComponentSingleFile ( externalLinkOpener = new ExternalLinkOpener ( ) , topMostOverlayContent . Add ) ;
2018-12-06 10:55:58 +08:00
2019-06-11 13:28:52 +08:00
chatOverlay . State . ValueChanged + = state = > channelManager . HighPollRate . Value = state . NewValue = = Visibility . Visible ;
2018-12-10 20:08:14 +08:00
2018-10-24 04:03:00 +08:00
Add ( externalLinkOpener = new ExternalLinkOpener ( ) ) ;
2018-07-13 19:32:22 +08:00
var singleDisplaySideOverlays = new OverlayContainer [ ] { settings , notifications } ;
overlays . AddRange ( singleDisplaySideOverlays ) ;
2018-06-06 15:17:51 +08:00
2018-07-13 19:32:22 +08:00
foreach ( var overlay in singleDisplaySideOverlays )
2018-04-13 17:19:50 +08:00
{
2019-06-11 13:28:52 +08:00
overlay . State . ValueChanged + = state = >
2018-04-13 17:19:50 +08:00
{
2019-06-11 13:28:52 +08:00
if ( state . NewValue = = Visibility . Hidden ) return ;
2019-02-28 12:31:40 +08:00
2018-07-13 19:32:22 +08:00
singleDisplaySideOverlays . Where ( o = > o ! = overlay ) . ForEach ( o = > o . Hide ( ) ) ;
2018-04-13 17:19:50 +08:00
} ;
}
2018-07-13 19:32:22 +08:00
// eventually informational overlays should be displayed in a stack, but for now let's only allow one to stay open at a time.
2019-05-31 12:23:50 +08:00
var informationalOverlays = new OverlayContainer [ ] { beatmapSetOverlay , userProfile } ;
2018-07-13 19:32:22 +08:00
overlays . AddRange ( informationalOverlays ) ;
2018-06-06 15:17:51 +08:00
2018-07-13 19:32:22 +08:00
foreach ( var overlay in informationalOverlays )
2018-04-13 17:19:50 +08:00
{
2019-06-11 13:28:52 +08:00
overlay . State . ValueChanged + = state = >
2018-04-13 17:19:50 +08:00
{
2019-06-11 13:28:52 +08:00
if ( state . NewValue = = Visibility . Hidden ) return ;
2019-02-28 12:31:40 +08:00
2018-07-13 19:32:22 +08:00
informationalOverlays . Where ( o = > o ! = overlay ) . ForEach ( o = > o . Hide ( ) ) ;
2018-04-13 17:19:50 +08:00
} ;
}
2018-07-13 19:32:22 +08:00
// ensure only one of these overlays are open at once.
2019-05-31 12:23:50 +08:00
var singleDisplayOverlays = new OverlayContainer [ ] { chatOverlay , social , direct , changelogOverlay } ;
2018-07-13 19:32:22 +08:00
overlays . AddRange ( singleDisplayOverlays ) ;
2018-06-06 15:17:51 +08:00
2018-07-13 19:32:22 +08:00
foreach ( var overlay in singleDisplayOverlays )
2018-04-13 17:19:50 +08:00
{
2019-06-11 13:28:52 +08:00
overlay . State . ValueChanged + = state = >
2018-04-13 17:19:50 +08:00
{
2018-07-13 19:32:22 +08:00
// informational overlays should be dismissed on a show or hide of a full overlay.
informationalOverlays . ForEach ( o = > o . Hide ( ) ) ;
2019-06-11 13:28:52 +08:00
if ( state . NewValue = = Visibility . Hidden ) return ;
2018-04-13 17:19:50 +08:00
2018-07-13 19:32:22 +08:00
singleDisplayOverlays . Where ( o = > o ! = overlay ) . ForEach ( o = > o . Hide ( ) ) ;
2018-04-13 17:19:50 +08:00
} ;
}
2019-02-22 16:51:39 +08:00
OverlayActivationMode . ValueChanged + = mode = >
2018-06-06 15:17:51 +08:00
{
2019-02-22 16:51:39 +08:00
if ( mode . NewValue ! = OverlayActivation . All ) CloseAllOverlays ( ) ;
2018-06-06 15:17:51 +08:00
} ;
2018-04-13 17:19:50 +08:00
void updateScreenOffset ( )
{
float offset = 0 ;
2019-06-11 13:28:52 +08:00
if ( settings . State . Value = = Visibility . Visible )
2018-04-13 17:19:50 +08:00
offset + = ToolbarButton . WIDTH / 2 ;
2019-06-11 13:28:52 +08:00
if ( notifications . State . Value = = Visibility . Visible )
2018-04-13 17:19:50 +08:00
offset - = ToolbarButton . WIDTH / 2 ;
2019-05-14 09:45:05 +08:00
screenContainer . MoveToX ( offset , SettingsPanel . TRANSITION_LENGTH , Easing . OutQuint ) ;
2018-04-13 17:19:50 +08:00
}
2019-06-11 13:28:52 +08:00
settings . State . ValueChanged + = _ = > updateScreenOffset ( ) ;
notifications . State . ValueChanged + = _ = > updateScreenOffset ( ) ;
2018-05-28 19:43:47 +08:00
}
2018-04-13 17:19:50 +08:00
2018-12-26 17:39:57 +08:00
public class GameIdleTracker : IdleTracker
{
2019-01-23 19:37:56 +08:00
private InputManager inputManager ;
2018-12-26 17:39:57 +08:00
public GameIdleTracker ( int time )
: base ( time )
{
}
2019-01-23 19:37:56 +08:00
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
inputManager = GetContainingInputManager ( ) ;
}
protected override bool AllowIdle = > inputManager . FocusedDrawable = = null ;
2018-12-26 17:39:57 +08:00
}
2018-04-13 17:19:50 +08:00
private void forwardLoggedErrorsToNotifications ( )
{
2018-06-21 13:43:38 +08:00
int recentLogCount = 0 ;
2018-04-13 17:19:50 +08:00
const double debounce = 5000 ;
Logger . NewEntry + = entry = >
{
2018-06-21 13:43:38 +08:00
if ( entry . Level < LogLevel . Important | | entry . Target = = null ) return ;
2018-04-13 17:19:50 +08:00
2018-06-21 13:50:42 +08:00
const int short_term_display_limit = 3 ;
2018-06-21 13:43:38 +08:00
2018-06-21 13:50:42 +08:00
if ( recentLogCount < short_term_display_limit )
{
2018-06-21 13:43:38 +08:00
Schedule ( ( ) = > notifications . Post ( new SimpleNotification
2018-04-13 17:19:50 +08:00
{
2019-04-02 18:55:24 +08:00
Icon = entry . Level = = LogLevel . Important ? FontAwesome . Solid . ExclamationCircle : FontAwesome . Solid . Bomb ,
2018-08-17 12:18:48 +08:00
Text = entry . Message + ( entry . Exception ! = null & & IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string . Empty ) ,
2018-06-21 13:50:42 +08:00
} ) ) ;
}
else if ( recentLogCount = = short_term_display_limit )
{
Schedule ( ( ) = > notifications . Post ( new SimpleNotification
{
2019-04-02 18:55:24 +08:00
Icon = FontAwesome . Solid . EllipsisH ,
2018-06-21 13:50:42 +08:00
Text = "Subsequent messages have been logged. Click to view log files." ,
2018-04-13 17:19:50 +08:00
Activated = ( ) = >
{
Host . Storage . GetStorageForDirectory ( "logs" ) . OpenInNativeExplorer ( ) ;
return true ;
}
2018-06-21 13:43:38 +08:00
} ) ) ;
2018-04-13 17:19:50 +08:00
}
2018-06-21 13:43:38 +08:00
Interlocked . Increment ( ref recentLogCount ) ;
Scheduler . AddDelayed ( ( ) = > Interlocked . Decrement ( ref recentLogCount ) , debounce ) ;
2018-04-13 17:19:50 +08:00
} ;
}
private Task asyncLoadStream ;
2019-05-15 16:36:29 +08:00
private T loadComponentSingleFile < T > ( T d , Action < T > add , bool cache = false )
2018-04-13 17:19:50 +08:00
where T : Drawable
{
2019-05-13 16:10:25 +08:00
if ( cache )
dependencies . Cache ( d ) ;
2018-04-13 17:19:50 +08:00
// schedule is here to ensure that all component loads are done after LoadComplete is run (and thus all dependencies are cached).
// with some better organisation of LoadComplete to do construction and dependency caching in one step, followed by calls to loadComponentSingleFile,
// we could avoid the need for scheduling altogether.
2018-08-20 15:06:12 +08:00
Schedule ( ( ) = >
2018-08-20 13:42:37 +08:00
{
2018-08-29 14:11:02 +08:00
var previousLoadStream = asyncLoadStream ;
//chain with existing load stream
asyncLoadStream = Task . Run ( async ( ) = >
2018-08-20 13:42:37 +08:00
{
2018-08-29 14:11:02 +08:00
if ( previousLoadStream ! = null )
await previousLoadStream ;
try
2018-08-20 15:06:12 +08:00
{
2018-12-07 18:39:54 +08:00
Logger . Log ( $"Loading {d}..." , level : LogLevel . Debug ) ;
2019-02-28 11:24:56 +08:00
2019-02-28 16:17:51 +08:00
// Since this is running in a separate thread, it is possible for OsuGame to be disposed after LoadComponentAsync has been called
// throwing an exception. To avoid this, the call is scheduled on the update thread, which does not run if IsDisposed = true
Task task = null ;
var del = new ScheduledDelegate ( ( ) = > task = LoadComponentAsync ( d , add ) ) ;
Scheduler . Add ( del ) ;
// The delegate won't complete if OsuGame has been disposed in the meantime
while ( ! IsDisposed & & ! del . Completed )
await Task . Delay ( 10 ) ;
// Either we're disposed or the load process has started successfully
2019-02-28 11:24:56 +08:00
if ( IsDisposed )
return ;
2019-02-28 16:17:51 +08:00
Debug . Assert ( task ! = null ) ;
await task ;
2018-12-07 18:39:54 +08:00
Logger . Log ( $"Loaded {d}!" , level : LogLevel . Debug ) ;
2018-08-29 14:11:02 +08:00
}
catch ( OperationCanceledException )
{
}
} ) ;
2018-08-20 15:06:12 +08:00
} ) ;
2019-05-15 16:36:29 +08:00
return d ;
2018-04-13 17:19:50 +08:00
}
public bool OnPressed ( GlobalAction action )
{
2019-01-23 19:52:00 +08:00
if ( introScreen = = null ) return false ;
2018-04-13 17:19:50 +08:00
switch ( action )
{
case GlobalAction . ToggleChat :
2018-11-23 10:00:17 +08:00
chatOverlay . ToggleVisibility ( ) ;
2018-04-13 17:19:50 +08:00
return true ;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case GlobalAction . ToggleSocial :
social . ToggleVisibility ( ) ;
return true ;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case GlobalAction . ResetInputSettings :
var sensitivity = frameworkConfig . GetBindable < double > ( FrameworkSetting . CursorSensitivity ) ;
sensitivity . Disabled = false ;
sensitivity . Value = 1 ;
sensitivity . Disabled = true ;
2018-04-13 20:46:17 +08:00
frameworkConfig . Set ( FrameworkSetting . IgnoredInputHandlers , string . Empty ) ;
2018-04-13 17:19:50 +08:00
frameworkConfig . GetBindable < ConfineMouseMode > ( FrameworkSetting . ConfineMouseMode ) . SetDefault ( ) ;
return true ;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case GlobalAction . ToggleToolbar :
Toolbar . ToggleVisibility ( ) ;
return true ;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case GlobalAction . ToggleSettings :
settings . ToggleVisibility ( ) ;
return true ;
2019-04-01 11:16:05 +08:00
2018-04-13 17:19:50 +08:00
case GlobalAction . ToggleDirect :
direct . ToggleVisibility ( ) ;
return true ;
2019-04-01 11:16:05 +08:00
2018-05-02 18:42:03 +08:00
case GlobalAction . ToggleGameplayMouseButtons :
2018-05-02 18:37:47 +08:00
LocalConfig . Set ( OsuSetting . MouseDisableButtons , ! LocalConfig . Get < bool > ( OsuSetting . MouseDisableButtons ) ) ;
return true ;
2018-04-13 17:19:50 +08:00
}
return false ;
}
2019-06-17 22:25:16 +08:00
#region Inactive audio dimming
2019-06-17 22:24:52 +08:00
private readonly BindableDouble inactiveVolumeFade = new BindableDouble ( ) ;
2018-04-13 17:19:50 +08:00
2019-02-19 18:16:03 +08:00
private void updateActiveState ( bool isActive )
2018-04-13 17:19:50 +08:00
{
2019-02-19 18:16:03 +08:00
if ( isActive )
2019-06-18 00:32:52 +08:00
this . TransformBindableTo ( inactiveVolumeFade , 1 , 400 , Easing . OutQuint ) ;
2019-02-19 18:16:03 +08:00
else
2019-06-18 00:32:52 +08:00
this . TransformBindableTo ( inactiveVolumeFade , LocalConfig . Get < double > ( OsuSetting . VolumeInactive ) , 4000 , Easing . OutQuint ) ;
2018-04-13 17:19:50 +08:00
}
2019-06-17 22:25:16 +08:00
#endregion
2018-04-13 17:19:50 +08:00
public bool OnReleased ( GlobalAction action ) = > false ;
private Container overlayContent ;
2019-04-05 15:00:21 +08:00
private Container rightFloatingOverlayContent ;
private Container leftFloatingOverlayContent ;
2019-01-21 18:34:35 +08:00
2019-03-22 02:16:10 +08:00
private Container topMostOverlayContent ;
2019-03-20 12:30:24 +08:00
2018-04-13 17:19:50 +08:00
private FrameworkConfigManager frameworkConfig ;
2019-06-20 22:40:25 +08:00
2019-01-04 12:29:37 +08:00
private ScalingContainer screenContainer ;
2018-04-13 17:19:50 +08:00
2019-06-20 22:40:25 +08:00
private MusicController musicController ;
2019-01-24 19:13:29 +08:00
protected override bool OnExiting ( )
{
if ( screenStack . CurrentScreen is Loader )
return false ;
if ( introScreen = = null )
return true ;
if ( ! introScreen . DidLoadMenu | | ! ( screenStack . CurrentScreen is Intro ) )
{
Scheduler . Add ( introScreen . MakeCurrent ) ;
return true ;
}
return base . OnExiting ( ) ;
}
2018-04-13 17:19:50 +08:00
/// <summary>
/// Use to programatically exit the game as if the user was triggering via alt-f4.
/// Will keep persisting until an exit occurs (exit may be blocked multiple times).
/// </summary>
public void GracefullyExit ( )
{
if ( ! OnExiting ( ) )
Exit ( ) ;
else
Scheduler . AddDelayed ( GracefullyExit , 2000 ) ;
}
protected override void UpdateAfterChildren ( )
{
base . UpdateAfterChildren ( ) ;
2019-01-08 11:57:31 +08:00
screenContainer . Padding = new MarginPadding { Top = ToolbarOffset } ;
2019-01-21 18:34:35 +08:00
overlayContent . Padding = new MarginPadding { Top = ToolbarOffset } ;
2018-04-13 17:19:50 +08:00
2019-01-23 19:52:00 +08:00
MenuCursorContainer . CanShowCursor = ( screenStack . CurrentScreen as IOsuScreen ) ? . CursorVisible ? ? false ;
2018-04-13 17:19:50 +08:00
}
2019-02-01 14:42:15 +08:00
protected virtual void ScreenChanged ( IScreen current , IScreen newScreen )
2018-04-13 17:19:50 +08:00
{
2019-01-23 19:52:00 +08:00
switch ( newScreen )
{
case Intro intro :
introScreen = intro ;
break ;
2019-04-01 11:16:05 +08:00
2019-01-23 19:52:00 +08:00
case MainMenu menu :
menuScreen = menu ;
break ;
}
2019-01-28 14:41:54 +08:00
if ( newScreen is IOsuScreen newOsuScreen )
{
OverlayActivationMode . Value = newOsuScreen . InitialOverlayActivationMode ;
if ( newOsuScreen . HideOverlaysOnEnter )
CloseAllOverlays ( ) ;
else
2019-06-11 13:28:52 +08:00
Toolbar . Show ( ) ;
2019-06-25 15:55:49 +08:00
2019-06-25 17:38:14 +08:00
if ( newOsuScreen . AllowBackButton )
2019-06-25 17:30:43 +08:00
backButton . Show ( ) ;
else
backButton . Hide ( ) ;
2019-01-28 14:41:54 +08:00
}
2018-12-27 18:18:27 +08:00
}
2019-01-23 19:52:00 +08:00
private void screenPushed ( IScreen lastScreen , IScreen newScreen )
2018-12-27 18:18:27 +08:00
{
2019-01-23 19:52:00 +08:00
ScreenChanged ( lastScreen , newScreen ) ;
Logger . Log ( $"Screen changed → {newScreen}" ) ;
2018-04-13 17:19:50 +08:00
}
2019-01-23 19:52:00 +08:00
private void screenExited ( IScreen lastScreen , IScreen newScreen )
2018-04-13 17:19:50 +08:00
{
2019-01-23 19:52:00 +08:00
ScreenChanged ( lastScreen , newScreen ) ;
Logger . Log ( $"Screen changed ← {newScreen}" ) ;
2018-04-13 17:19:50 +08:00
if ( newScreen = = null )
Exit ( ) ;
}
}
}