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 System.Linq ;
using System.Threading ;
using System.Threading.Tasks ;
2020-07-24 13:10:05 +08:00
using Humanizer ;
2020-05-09 18:13:18 +08:00
using JetBrains.Annotations ;
2021-11-22 16:40:43 +08:00
using osu.Framework.Allocation ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Audio ;
2019-02-21 18:04:31 +08:00
using osu.Framework.Bindables ;
2021-11-22 16:40:43 +08:00
using osu.Framework.Configuration ;
2018-07-13 19:32:22 +08:00
using osu.Framework.Extensions.IEnumerableExtensions ;
2021-11-22 16:40:43 +08:00
using osu.Framework.Graphics ;
using osu.Framework.Graphics.Containers ;
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 ;
2021-09-16 17:26:12 +08:00
using osu.Framework.Input.Events ;
2021-11-22 16:40:43 +08:00
using osu.Framework.Logging ;
using osu.Framework.Screens ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Threading ;
2018-07-11 00:32:10 +08:00
using osu.Game.Beatmaps ;
2020-09-05 02:52:07 +08:00
using osu.Game.Collections ;
2021-11-22 16:40:43 +08:00
using osu.Game.Configuration ;
using osu.Game.Database ;
using osu.Game.Extensions ;
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.Input.Bindings ;
2021-11-22 16:40:43 +08:00
using osu.Game.IO ;
using osu.Game.Localisation ;
using osu.Game.Online.API.Requests.Responses ;
2018-09-14 11:06:04 +08:00
using osu.Game.Online.Chat ;
2021-11-22 16:40:43 +08:00
using osu.Game.Overlays ;
2020-09-04 15:22:37 +08:00
using osu.Game.Overlays.Music ;
2021-11-22 16:40:43 +08:00
using osu.Game.Overlays.Notifications ;
using osu.Game.Overlays.Toolbar ;
2018-04-13 17:19:50 +08:00
using osu.Game.Overlays.Volume ;
2021-11-22 16:40:43 +08:00
using osu.Game.Performance ;
2019-12-26 13:52:08 +08:00
using osu.Game.Rulesets.Mods ;
2018-11-28 15:12:57 +08:00
using osu.Game.Scoring ;
2021-11-22 16:40:43 +08:00
using osu.Game.Screens ;
using osu.Game.Screens.Menu ;
2020-06-15 19:23:35 +08:00
using osu.Game.Screens.Play ;
using osu.Game.Screens.Ranking ;
2018-07-11 00:32:10 +08:00
using osu.Game.Screens.Select ;
2021-11-22 16:40:43 +08:00
using osu.Game.Skinning ;
2021-04-29 16:20:22 +08:00
using osu.Game.Skinning.Editor ;
2021-11-22 16:40:43 +08:00
using osu.Game.Updater ;
2021-11-05 12:53:00 +08:00
using osu.Game.Users ;
2021-11-22 16:40:43 +08:00
using osu.Game.Utils ;
using osuTK.Graphics ;
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>
2022-04-18 16:49:28 +08:00
public class OsuGame : OsuGameBase , IKeyBindingHandler < GlobalAction > , ILocalUserPlayInfo , IPerformFromScreenRunner
2018-04-13 17:19:50 +08:00
{
2021-08-13 15:29:36 +08:00
/// <summary>
/// The amount of global offset to apply when a left/right anchored overlay is displayed (ie. settings or notifications).
/// </summary>
2021-08-13 15:35:22 +08:00
protected const float SIDE_OVERLAY_OFFSET_RATIO = 0.05f ;
2021-08-07 03:36:40 +08:00
2018-04-13 17:19:50 +08:00
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
2020-07-19 10:37:38 +08:00
[NotNull]
2021-08-07 03:36:40 +08:00
protected readonly NotificationOverlay Notifications = new NotificationOverlay ( ) ;
2018-04-13 17:19:50 +08:00
2020-04-21 15:00:00 +08:00
private BeatmapListingOverlay beatmapListing ;
2020-01-12 03:43:51 +08:00
2020-04-16 17:05:51 +08:00
private DashboardOverlay dashboard ;
2018-04-13 17:19:50 +08:00
2020-07-16 19:48:40 +08:00
private NewsOverlay news ;
2018-04-13 17:19:50 +08:00
private UserProfileOverlay userProfile ;
private BeatmapSetOverlay beatmapSetOverlay ;
2021-04-22 17:16:12 +08:00
private WikiOverlay wikiOverlay ;
2021-10-12 10:41:59 +08:00
private ChangelogOverlay changelogOverlay ;
2021-05-03 14:15:50 +08:00
private SkinEditorOverlay skinEditor ;
2021-05-06 13:17:30 +08:00
private Container overlayContent ;
private Container rightFloatingOverlayContent ;
private Container leftFloatingOverlayContent ;
private Container topMostOverlayContent ;
2021-12-13 11:48:15 +08:00
protected ScalingContainer ScreenContainer { get ; private set ; }
2021-05-06 13:17:30 +08:00
2021-08-07 23:52:27 +08:00
protected Container ScreenOffsetContainer { get ; private set ; }
2021-05-06 13:17:30 +08:00
2021-08-29 11:13:01 +08:00
private Container overlayOffsetContainer ;
2021-05-06 13:17:30 +08:00
[Resolved]
private FrameworkConfigManager frameworkConfig { get ; set ; }
2020-12-22 13:28:26 +08:00
[Cached]
private readonly DifficultyRecommender difficultyRecommender = new DifficultyRecommender ( ) ;
2021-05-09 23:12:58 +08:00
[Cached]
2021-11-25 16:12:15 +08:00
private readonly LegacyImportManager legacyImportManager = new LegacyImportManager ( ) ;
2021-05-09 23:12:58 +08:00
2018-10-02 09:12:07 +08:00
[Cached]
private readonly ScreenshotManager screenshotManager = new ScreenshotManager ( ) ;
2018-04-13 20:13:09 +08:00
2019-11-12 22:08:16 +08:00
protected SentryLogger SentryLogger ;
2018-08-03 18:25:55 +08:00
2021-01-25 02:46:10 +08:00
public virtual StableStorage GetStorageForStableInstall ( ) = > null ;
2018-04-13 17:19:50 +08:00
2021-08-29 11:13:01 +08:00
private float toolbarOffset = > ( Toolbar ? . Position . Y ? ? 0 ) + ( Toolbar ? . DrawHeight ? ? 0 ) ;
2018-04-13 17:19:50 +08:00
2018-11-20 01:48:59 +08:00
private IdleTracker idleTracker ;
2020-09-03 02:55:26 +08:00
/// <summary>
/// Whether overlays should be able to be opened game-wide. Value is sourced from the current active screen.
/// </summary>
2020-08-28 02:07:24 +08:00
public readonly IBindable < OverlayActivation > OverlayActivationMode = new Bindable < OverlayActivation > ( ) ;
2018-04-13 17:19:50 +08:00
2020-10-06 20:09:35 +08:00
/// <summary>
2020-10-07 13:44:49 +08:00
/// Whether the local user is currently interacting with the game in a way that should not be interrupted.
2020-10-06 20:09:35 +08:00
/// </summary>
2020-10-08 17:25:40 +08:00
/// <remarks>
/// This is exclusively managed by <see cref="Player"/>. If other components are mutating this state, a more
/// resilient method should be used to ensure correct state.
/// </remarks>
public Bindable < bool > LocalUserPlaying = new BindableBool ( ) ;
2018-04-13 17:19:50 +08:00
2019-07-29 13:30:46 +08:00
protected OsuScreenStack ScreenStack ;
2019-07-31 18:47:41 +08:00
2019-07-29 13:30:46 +08:00
protected BackButton BackButton ;
2020-05-12 11:49:35 +08:00
protected SettingsOverlay Settings ;
2019-07-31 18:47:41 +08:00
2018-04-13 17:19:50 +08:00
private VolumeOverlay volume ;
2022-02-18 15:06:38 +08:00
2019-01-23 19:52:00 +08:00
private OsuLogo osuLogo ;
private MainMenu menuScreen ;
2019-07-09 16:59:40 +08:00
2022-01-16 02:42:38 +08:00
private VersionManager versionManager ;
2020-05-09 18:13:18 +08:00
[CanBeNull]
2019-07-09 16:59:40 +08:00
private IntroScreen introScreen ;
2018-04-13 17:19:50 +08:00
2021-11-22 16:40:43 +08:00
private Bindable < string > configRuleset ;
2018-04-13 17:19:50 +08:00
2021-10-28 12:09:03 +08:00
private Bindable < float > uiScale ;
2021-11-23 15:04:55 +08:00
private Bindable < string > configSkin ;
2018-04-13 17:19:50 +08:00
private readonly string [ ] args ;
2021-09-01 05:29:16 +08:00
private readonly List < OsuFocusedOverlayContainer > focusedOverlays = new List < OsuFocusedOverlayContainer > ( ) ;
2018-06-06 15:17:51 +08:00
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
2019-11-12 22:08:16 +08:00
SentryLogger = new SentryLogger ( this ) ;
2018-04-13 17:19:50 +08:00
}
2019-03-01 11:20:31 +08:00
private void updateBlockingOverlayFade ( ) = >
2021-12-13 11:48:15 +08:00
ScreenContainer . FadeColour ( visibleBlockingOverlays . Any ( ) ? OsuColour . Gray ( 0.5f ) : Color4 . White , 500 , Easing . OutQuint ) ;
2019-03-01 11:20:31 +08:00
public void AddBlockingOverlay ( OverlayContainer overlay )
{
if ( ! visibleBlockingOverlays . Contains ( overlay ) )
visibleBlockingOverlays . Add ( overlay ) ;
updateBlockingOverlayFade ( ) ;
}
2021-01-04 16:49:11 +08:00
public void RemoveBlockingOverlay ( OverlayContainer overlay ) = > Schedule ( ( ) = >
2019-03-01 11:20:31 +08:00
{
2019-03-01 12:29:02 +08:00
visibleBlockingOverlays . Remove ( overlay ) ;
2019-03-01 11:20:31 +08:00
updateBlockingOverlayFade ( ) ;
2021-01-04 16:49:11 +08:00
} ) ;
2019-03-01 11:20:31 +08:00
2018-06-06 15:17:51 +08:00
/// <summary>
/// Close all game-wide overlays.
/// </summary>
2019-11-08 22:04:18 +08:00
/// <param name="hideToolbar">Whether the toolbar should also be hidden.</param>
public void CloseAllOverlays ( bool hideToolbar = true )
2018-06-06 15:17:51 +08:00
{
2021-09-01 05:29:16 +08:00
foreach ( var overlay in focusedOverlays )
2019-06-11 13:28:52 +08:00
overlay . Hide ( ) ;
2019-05-12 21:34:36 +08:00
2019-11-08 22:04:18 +08:00
if ( hideToolbar ) Toolbar . Hide ( ) ;
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]
2020-02-14 21:14:00 +08:00
private void load ( )
2018-04-13 17:19:50 +08:00
{
dependencies . CacheAs ( this ) ;
2019-11-12 22:08:16 +08:00
dependencies . Cache ( SentryLogger ) ;
2018-08-03 18:25:55 +08:00
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
2021-11-22 16:40:43 +08:00
configRuleset = LocalConfig . GetBindable < string > ( OsuSetting . Ruleset ) ;
2021-10-28 12:09:03 +08:00
uiScale = LocalConfig . GetBindable < float > ( OsuSetting . UIScale ) ;
2021-07-01 18:03:55 +08:00
2021-11-22 16:40:43 +08:00
var preferredRuleset = int . TryParse ( configRuleset . Value , out int rulesetId )
// int parsing can be removed 20220522
? RulesetStore . GetRuleset ( rulesetId )
: RulesetStore . GetRuleset ( configRuleset . Value ) ;
2021-07-01 18:03:55 +08:00
try
{
Ruleset . Value = preferredRuleset ? ? RulesetStore . AvailableRulesets . First ( ) ;
}
catch ( Exception e )
{
// on startup, a ruleset may be selected which has compatibility issues.
Logger . Error ( e , $@"Failed to switch to preferred ruleset {preferredRuleset}." ) ;
Ruleset . Value = RulesetStore . AvailableRulesets . First ( ) ;
}
2021-11-22 16:40:43 +08:00
Ruleset . ValueChanged + = r = > configRuleset . Value = r . NewValue . ShortName ;
2018-04-13 17:19:50 +08:00
// bind config int to database SkinInfo
2021-11-23 15:04:55 +08:00
configSkin = LocalConfig . GetBindable < string > ( OsuSetting . Skin ) ;
SkinManager . CurrentSkinInfo . ValueChanged + = skin = > configSkin . Value = skin . NewValue . ID . ToString ( ) ;
2019-08-29 15:38:39 +08:00
configSkin . ValueChanged + = skinId = >
{
2022-01-26 12:37:33 +08:00
Live < SkinInfo > skinInfo = null ;
2021-11-23 15:04:55 +08:00
if ( Guid . TryParse ( skinId . NewValue , out var guid ) )
skinInfo = SkinManager . Query ( s = > s . ID = = guid ) ;
2019-08-29 15:38:39 +08:00
if ( skinInfo = = null )
{
2021-11-23 15:04:55 +08:00
if ( guid = = SkinInfo . CLASSIC_SKIN )
2021-12-14 13:21:23 +08:00
skinInfo = DefaultLegacySkin . CreateInfo ( ) . ToLiveUnmanaged ( ) ;
2019-08-29 15:38:39 +08:00
}
2021-12-14 13:21:23 +08:00
SkinManager . CurrentSkinInfo . Value = skinInfo ? ? DefaultSkin . CreateInfo ( ) . ToLiveUnmanaged ( ) ;
2019-08-29 15:38:39 +08:00
} ;
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-12-26 13:52:08 +08:00
SelectedMods . BindValueChanged ( modsChanged ) ;
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
2019-11-01 10:22:32 +08:00
/// <summary>
/// Handle an arbitrary URL. Displays via in-game overlays where possible.
/// This can be called from a non-thread-safe non-game-loaded state.
/// </summary>
/// <param name="url">The URL to load.</param>
2019-11-03 12:16:54 +08:00
public void HandleLink ( string url ) = > HandleLink ( MessageFormatter . GetLinkDetails ( url ) ) ;
2019-11-01 10:40:51 +08:00
2019-11-01 10:22:32 +08:00
/// <summary>
/// Handle a specific <see cref="LinkDetails"/>.
/// This can be called from a non-thread-safe non-game-loaded state.
/// </summary>
/// <param name="link">The link to load.</param>
2019-11-03 12:16:54 +08:00
public void HandleLink ( LinkDetails link ) = > Schedule ( ( ) = >
2019-11-01 10:40:51 +08:00
{
2021-11-08 13:17:47 +08:00
string argString = link . Argument . ToString ( ) ;
2019-11-01 10:40:51 +08:00
switch ( link . Action )
{
case LinkAction . OpenBeatmap :
// TODO: proper query params handling
2021-11-08 13:17:47 +08:00
if ( int . TryParse ( argString . Contains ( '?' ) ? argString . Split ( '?' ) [ 0 ] : argString , out int beatmapId ) )
2019-11-01 10:40:51 +08:00
ShowBeatmap ( beatmapId ) ;
break ;
case LinkAction . OpenBeatmapSet :
2021-11-08 13:17:47 +08:00
if ( int . TryParse ( argString , out int setId ) )
2019-11-01 10:40:51 +08:00
ShowBeatmapSet ( setId ) ;
break ;
case LinkAction . OpenChannel :
2021-11-08 13:17:47 +08:00
ShowChannel ( argString ) ;
2019-11-01 10:40:51 +08:00
break ;
2020-01-31 06:41:50 +08:00
case LinkAction . SearchBeatmapSet :
2021-11-08 13:17:47 +08:00
SearchBeatmapSet ( argString ) ;
2020-01-30 12:30:25 +08:00
break ;
2019-11-01 10:40:51 +08:00
case LinkAction . OpenEditorTimestamp :
case LinkAction . JoinMultiplayerMatch :
case LinkAction . Spectate :
2021-08-07 03:36:40 +08:00
waitForReady ( ( ) = > Notifications , _ = > Notifications . Post ( new SimpleNotification
2019-11-01 10:22:32 +08:00
{
Text = @"This link type is not yet supported!" ,
Icon = FontAwesome . Solid . LifeRing ,
} ) ) ;
2019-11-01 10:40:51 +08:00
break ;
case LinkAction . External :
2021-11-08 13:17:47 +08:00
OpenUrlExternally ( argString ) ;
2019-11-01 10:40:51 +08:00
break ;
case LinkAction . OpenUserProfile :
2021-11-08 13:17:47 +08:00
if ( ! ( link . Argument is IUser user ) )
{
user = int . TryParse ( argString , out int userId )
? new APIUser { Id = userId }
: new APIUser { Username = argString } ;
}
ShowUser ( user ) ;
2021-08-30 20:22:12 +08:00
2019-11-01 10:40:51 +08:00
break ;
2021-05-17 01:43:59 +08:00
case LinkAction . OpenWiki :
2021-11-08 13:17:47 +08:00
ShowWiki ( argString ) ;
2021-05-17 01:43:59 +08:00
break ;
2021-10-12 10:43:32 +08:00
case LinkAction . OpenChangelog :
2021-11-08 13:17:47 +08:00
if ( string . IsNullOrEmpty ( argString ) )
2021-10-12 10:43:32 +08:00
ShowChangelogListing ( ) ;
else
{
2021-11-08 13:17:47 +08:00
string [ ] changelogArgs = argString . Split ( "/" ) ;
2021-10-12 10:43:32 +08:00
ShowChangelogBuild ( changelogArgs [ 0 ] , changelogArgs [ 1 ] ) ;
}
break ;
2019-11-01 10:40:51 +08:00
default :
throw new NotImplementedException ( $"This {nameof(LinkAction)} ({link.Action.ToString()}) is missing an associated action." ) ;
}
2019-11-03 12:16:54 +08:00
} ) ;
2019-11-01 10:40:51 +08:00
2022-01-27 13:53:11 +08:00
public void OpenUrlExternally ( string url , bool bypassExternalUrlWarning = false ) = > waitForReady ( ( ) = > externalLinkOpener , _ = >
2018-12-06 11:17:08 +08:00
{
2020-10-16 17:27:02 +08:00
if ( url . StartsWith ( '/' ) )
2020-12-24 17:11:40 +08:00
url = $"{API.APIEndpointUrl}{url}" ;
2018-12-06 11:17:08 +08:00
2022-01-27 13:53:11 +08:00
externalLinkOpener . OpenUrlExternally ( url , bypassExternalUrlWarning ) ;
2019-11-01 10:22:32 +08:00
} ) ;
/// <summary>
/// Open a specific channel in chat.
/// </summary>
/// <param name="channel">The channel to display.</param>
public void ShowChannel ( string channel ) = > waitForReady ( ( ) = > channelManager , _ = >
{
try
{
channelManager . OpenChannel ( channel ) ;
}
catch ( ChannelNotFoundException )
{
Logger . Log ( $"The requested channel \" { channel } \ " does not exist" ) ;
}
} ) ;
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>
2019-11-01 10:22:32 +08:00
public void ShowBeatmapSet ( int setId ) = > waitForReady ( ( ) = > beatmapSetOverlay , _ = > 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>
2021-11-05 12:53:00 +08:00
/// <param name="user">The user to display.</param>
public void ShowUser ( IUser user ) = > waitForReady ( ( ) = > userProfile , _ = > userProfile . ShowUser ( user ) ) ;
2021-08-30 02:19:55 +08:00
2019-02-25 11:58:58 +08:00
/// <summary>
/// Show a beatmap's set as an overlay, displaying the given beatmap.
/// </summary>
/// <param name="beatmapId">The beatmap to show.</param>
2019-11-01 10:22:32 +08:00
public void ShowBeatmap ( int beatmapId ) = > waitForReady ( ( ) = > beatmapSetOverlay , _ = > beatmapSetOverlay . FetchAndShowBeatmap ( beatmapId ) ) ;
2019-02-25 11:58:58 +08:00
2020-01-30 12:30:25 +08:00
/// <summary>
2021-07-03 21:22:03 +08:00
/// Shows the beatmap listing overlay, with the given <paramref name="query"/> in the search box.
2020-01-30 12:30:25 +08:00
/// </summary>
2020-01-31 06:41:50 +08:00
/// <param name="query">The query to search for.</param>
2021-07-01 18:41:30 +08:00
public void SearchBeatmapSet ( string query ) = > waitForReady ( ( ) = > beatmapListing , _ = > beatmapListing . ShowWithSearch ( query ) ) ;
2021-05-17 01:43:59 +08:00
/// <summary>
/// Show a wiki's page as an overlay
/// </summary>
/// <param name="path">The wiki page to show</param>
public void ShowWiki ( string path ) = > waitForReady ( ( ) = > wikiOverlay , _ = > wikiOverlay . ShowPage ( path ) ) ;
2020-01-30 12:30:25 +08:00
2018-07-11 00:32:10 +08:00
/// <summary>
2021-10-12 10:42:29 +08:00
/// Show changelog listing overlay
/// </summary>
public void ShowChangelogListing ( ) = > waitForReady ( ( ) = > changelogOverlay , _ = > changelogOverlay . ShowListing ( ) ) ;
/// <summary>
/// Show changelog's build as an overlay
/// </summary>
/// <param name="updateStream">The update stream name</param>
/// <param name="version">The build version of the update stream</param>
public void ShowChangelogBuild ( string updateStream , string version ) = > waitForReady ( ( ) = > changelogOverlay , _ = > changelogOverlay . ShowBuild ( updateStream , version ) ) ;
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>
2020-11-21 20:26:09 +08:00
/// <param name="difficultyCriteria">Optional predicate used to narrow the set of difficulties to select from when presenting.</param>
/// <remarks>
/// Among items satisfying the predicate, the order of preference is:
/// <list type="bullet">
/// <item>beatmap with recommended difficulty, as provided by <see cref="DifficultyRecommender"/>,</item>
/// <item>first beatmap from the current ruleset,</item>
/// <item>first beatmap from any ruleset.</item>
/// </list>
/// </remarks>
2021-10-29 16:40:12 +08:00
public void PresentBeatmap ( IBeatmapSetInfo beatmap , Predicate < BeatmapInfo > difficultyCriteria = null )
2018-07-11 00:32:10 +08:00
{
2022-01-26 12:37:33 +08:00
Live < BeatmapSetInfo > databasedSet = null ;
2021-10-29 16:40:12 +08:00
if ( beatmap . OnlineID > 0 )
2021-11-12 16:50:31 +08:00
databasedSet = BeatmapManager . QueryBeatmapSet ( s = > s . OnlineID = = beatmap . OnlineID ) ;
2021-10-29 16:40:12 +08:00
if ( beatmap is BeatmapSetInfo localBeatmap )
databasedSet ? ? = BeatmapManager . QueryBeatmapSet ( s = > s . Hash = = localBeatmap . Hash ) ;
2019-02-25 11:58:58 +08:00
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 ;
}
2022-01-10 15:34:32 +08:00
var detachedSet = databasedSet . PerformRead ( s = > s . Detach ( ) ) ;
2020-01-30 22:34:04 +08:00
PerformFromScreen ( screen = >
2018-07-13 20:08:41 +08:00
{
2022-01-10 15:34:32 +08:00
// Find beatmaps that match our predicate.
var beatmaps = detachedSet . Beatmaps . Where ( b = > difficultyCriteria ? . Invoke ( b ) ? ? true ) . ToList ( ) ;
2019-02-24 11:08:27 +08:00
2022-01-10 15:34:32 +08:00
// Use all beatmaps if predicate matched nothing
if ( beatmaps . Count = = 0 )
beatmaps = detachedSet . Beatmaps . ToList ( ) ;
2019-04-02 23:57:31 +08:00
2022-01-10 15:34:32 +08:00
// Prefer recommended beatmap if recommendations are available, else fallback to a sane selection.
var selection = difficultyRecommender . GetRecommendedBeatmap ( beatmaps )
? ? beatmaps . FirstOrDefault ( b = > b . Ruleset . Equals ( Ruleset . Value ) )
? ? beatmaps . First ( ) ;
2019-02-24 11:08:27 +08:00
2022-01-10 15:34:32 +08:00
if ( screen is IHandlePresentBeatmap presentableScreen )
{
presentableScreen . PresentBeatmap ( BeatmapManager . GetWorkingBeatmap ( selection ) , selection . Ruleset ) ;
}
else
{
Ruleset . Value = selection . Ruleset ;
Beatmap . Value = BeatmapManager . GetWorkingBeatmap ( selection ) ;
}
2021-03-03 13:04:00 +08:00
} , validScreens : new [ ] { typeof ( SongSelect ) , typeof ( IHandlePresentBeatmap ) } ) ;
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>
2021-12-06 21:47:00 +08:00
public void PresentScore ( IScoreInfo score , ScorePresentType presentType = ScorePresentType . Results )
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.
2021-06-15 13:06:17 +08:00
ScoreInfo databasedScoreInfo = null ;
2021-12-10 14:28:41 +08:00
if ( score . OnlineID > 0 )
2022-01-07 23:40:14 +08:00
databasedScoreInfo = ScoreManager . Query ( s = > s . OnlineID = = score . OnlineID ) ;
2021-06-15 13:06:17 +08:00
2021-12-13 16:09:13 +08:00
if ( score is ScoreInfo scoreInfo )
2022-01-07 23:40:14 +08:00
databasedScoreInfo ? ? = ScoreManager . Query ( s = > s . Hash = = scoreInfo . Hash ) ;
2019-07-30 05:31:45 +08:00
if ( databasedScoreInfo = = null )
{
Logger . Log ( "The requested score could not be found locally." , LoggingTarget . Information ) ;
return ;
}
2019-06-29 18:38:48 +08:00
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
2021-10-04 16:35:53 +08:00
var databasedBeatmap = BeatmapManager . QueryBeatmap ( b = > b . ID = = databasedScoreInfo . BeatmapInfo . 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 ;
}
2020-01-30 22:34:04 +08:00
PerformFromScreen ( screen = >
2019-02-25 11:58:58 +08:00
{
2020-06-15 19:23:35 +08:00
Ruleset . Value = databasedScore . ScoreInfo . Ruleset ;
2019-02-25 11:58:58 +08:00
Beatmap . Value = BeatmapManager . GetWorkingBeatmap ( databasedBeatmap ) ;
2020-06-15 19:23:35 +08:00
switch ( presentType )
{
case ScorePresentType . Gameplay :
screen . Push ( new ReplayPlayerLoader ( databasedScore ) ) ;
break ;
case ScorePresentType . Results :
2020-11-20 13:35:44 +08:00
screen . Push ( new SoloResultsScreen ( databasedScore . ScoreInfo , false ) ) ;
2020-06-15 19:23:35 +08:00
break ;
}
2020-02-03 15:28:38 +08:00
} , validScreens : new [ ] { typeof ( PlaySongSelect ) } ) ;
2019-02-25 11:58:58 +08:00
}
2020-12-16 21:28:16 +08:00
public override Task Import ( params ImportTask [ ] imports )
2020-12-09 20:32:59 +08:00
{
2020-12-14 17:03:01 +08:00
// encapsulate task as we don't want to begin the import process until in a ready state.
2021-07-02 13:43:48 +08:00
// ReSharper disable once AsyncVoidLambda
// TODO: This is bad because `new Task` doesn't have a Func<Task?> override.
// Only used for android imports and a bit of a mess. Probably needs rethinking overall.
2021-03-08 11:57:16 +08:00
var importTask = new Task ( async ( ) = > await base . Import ( imports ) . ConfigureAwait ( false ) ) ;
2020-12-14 17:03:01 +08:00
2020-12-14 16:59:04 +08:00
waitForReady ( ( ) = > this , _ = > importTask . Start ( ) ) ;
2020-12-14 17:03:01 +08:00
2020-12-13 00:12:15 +08:00
return importTask ;
2019-02-25 11:58:58 +08:00
}
2019-09-25 21:13:49 +08:00
protected virtual Loader CreateLoader ( ) = > new Loader ( ) ;
2020-03-05 12:34:04 +08:00
protected virtual UpdateManager CreateUpdateManager ( ) = > new UpdateManager ( ) ;
2021-06-16 19:53:48 +08:00
protected virtual HighPerformanceSession CreateHighPerformanceSession ( ) = > new HighPerformanceSession ( ) ;
2019-11-11 12:58:35 +08:00
protected override Container CreateScalingContainer ( ) = > new ScalingContainer ( ScalingMode . Everything ) ;
2019-08-13 11:06:57 +08:00
#region Beatmap progression
2019-06-20 22:40:25 +08:00
private void beatmapChanged ( ValueChangedEvent < WorkingBeatmap > beatmap )
{
2020-03-04 18:09:52 +08:00
beatmap . OldValue ? . CancelAsyncLoad ( ) ;
2020-08-05 20:21:08 +08:00
beatmap . NewValue ? . BeginAsyncLoad ( ) ;
2021-10-14 13:07:43 +08:00
Logger . Log ( $"Game-wide working beatmap updated to {beatmap.NewValue}" ) ;
2019-06-20 22:40:25 +08:00
}
2019-12-26 13:52:08 +08:00
private void modsChanged ( ValueChangedEvent < IReadOnlyList < Mod > > mods )
{
2021-02-05 15:46:21 +08:00
// a lease may be taken on the mods bindable, at which point we can't really ensure valid mods.
if ( SelectedMods . Disabled )
return ;
2021-02-01 19:20:19 +08:00
if ( ! ModUtils . CheckValidForGameplay ( mods . NewValue , out var invalid ) )
{
// ensure we always have a valid set of mods.
SelectedMods . Value = mods . NewValue . Except ( invalid ) . ToArray ( ) ;
}
2019-12-26 13:52:08 +08:00
}
2019-06-20 22:40:25 +08:00
#endregion
2020-11-11 13:45:50 +08:00
private PerformFromMenuRunner performFromMainMenuTask ;
2019-02-25 11:58:58 +08:00
2020-02-11 21:37:38 +08:00
public void PerformFromScreen ( Action < IScreen > action , IEnumerable < Type > validScreens = null )
2019-02-25 11:58:58 +08:00
{
2019-02-25 13:01:51 +08:00
performFromMainMenuTask ? . Cancel ( ) ;
2020-11-11 13:45:50 +08:00
Add ( performFromMainMenuTask = new PerformFromMenuRunner ( action , validScreens , ( ) = > ScreenStack . CurrentScreen ) ) ;
2018-04-13 17:19:50 +08:00
}
2019-11-01 10:22:32 +08:00
/// <summary>
/// Wait for the game (and target component) to become loaded and then run an action.
/// </summary>
/// <param name="retrieveInstance">A function to retrieve a (potentially not-yet-constructed) target instance.</param>
/// <param name="action">The action to perform on the instance when load is confirmed.</param>
/// <typeparam name="T">The type of the target instance.</typeparam>
private void waitForReady < T > ( Func < T > retrieveInstance , Action < T > action )
where T : Drawable
{
var instance = retrieveInstance ( ) ;
if ( ScreenStack = = null | | ScreenStack . CurrentScreen is StartupScreen | | instance ? . IsLoaded ! = true )
Schedule ( ( ) = > waitForReady ( retrieveInstance , action ) ) ;
else
action ( instance ) ;
}
2018-08-03 18:25:55 +08:00
protected override void Dispose ( bool isDisposing )
{
base . Dispose ( isDisposing ) ;
2019-11-12 22:08:16 +08:00
SentryLogger . Dispose ( ) ;
2018-08-03 18:25:55 +08:00
}
2021-03-24 12:37:37 +08:00
protected override IDictionary < FrameworkSetting , object > GetFrameworkConfigDefaults ( )
= > new Dictionary < FrameworkSetting , object >
{
// General expectation that osu! starts in fullscreen by default (also gives the most predictable performance)
{ FrameworkSetting . WindowMode , WindowMode . Fullscreen }
} ;
2018-04-13 17:19:50 +08:00
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
2021-04-20 16:06:01 +08:00
foreach ( var language in Enum . GetValues ( typeof ( Language ) ) . OfType < Language > ( ) )
{
2022-04-19 13:30:45 +08:00
#if DEBUG
2022-04-19 15:49:41 +08:00
if ( language = = Language . debug )
2022-04-19 13:30:45 +08:00
{
2022-04-19 15:49:41 +08:00
Localisation . AddLanguage ( Language . debug . ToString ( ) , new DebugLocalisationStore ( ) ) ;
2022-04-19 13:30:45 +08:00
continue ;
}
#endif
2021-10-27 12:04:41 +08:00
string cultureCode = language . ToCultureCode ( ) ;
2021-06-19 14:00:36 +08:00
try
{
Localisation . AddLanguage ( cultureCode , new ResourceManagerLocalisationStore ( cultureCode ) ) ;
}
catch ( Exception ex )
{
Logger . Error ( ex , $"Could not load localisations for language \" { cultureCode } \ "" ) ;
}
2021-04-20 16:06:01 +08:00
}
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.
2021-08-07 03:36:40 +08:00
SkinManager . PostNotification = n = > Notifications . Post ( n ) ;
2018-04-13 17:19:50 +08:00
2021-08-07 03:36:40 +08:00
BeatmapManager . PostNotification = n = > Notifications . Post ( n ) ;
2021-10-15 15:00:09 +08:00
BeatmapManager . PostImport = items = > PresentBeatmap ( items . First ( ) . Value ) ;
2018-08-31 17:28:53 +08:00
2021-11-25 16:23:46 +08:00
BeatmapDownloader . PostNotification = n = > Notifications . Post ( n ) ;
2021-11-25 16:21:05 +08:00
ScoreDownloader . PostNotification = n = > Notifications . Post ( n ) ;
2021-08-07 03:36:40 +08:00
ScoreManager . PostNotification = n = > Notifications . Post ( n ) ;
2021-10-04 15:35:55 +08:00
ScoreManager . PostImport = items = > PresentScore ( items . First ( ) . Value ) ;
2018-04-13 17:19:50 +08:00
2020-11-11 11:19:01 +08:00
// make config aware of how to lookup skins for on-screen display purposes.
// if this becomes a more common thing, tracked settings should be reconsidered to allow local DI.
2021-11-29 16:15:26 +08:00
LocalConfig . LookupSkinName = id = > SkinManager . Query ( s = > s . ID = = id ) ? . ToString ( ) ? ? "Unknown" ;
2020-11-11 11:19:01 +08:00
2020-11-11 11:54:39 +08:00
LocalConfig . LookupKeyBindings = l = >
{
2021-06-18 16:01:51 +08:00
var combinations = KeyBindingStore . GetReadableKeyCombinationsFor ( l ) ;
2020-11-11 11:54:39 +08:00
2021-06-18 16:01:51 +08:00
if ( combinations . Count = = 0 )
2021-10-11 16:02:26 +08:00
return ToastStrings . NoKeyBound ;
2020-11-11 11:54:39 +08:00
2021-10-11 16:11:41 +08:00
return string . Join ( " / " , combinations ) ;
2020-11-11 11:54:39 +08:00
} ;
2019-01-23 19:52:00 +08:00
Container logoContainer ;
2019-07-30 11:00:04 +08:00
BackButton . Receptor receptor ;
2019-01-23 19:52:00 +08:00
2019-05-13 16:10:25 +08:00
dependencies . CacheAs ( idleTracker = new GameIdleTracker ( 6000 ) ) ;
2021-04-19 10:30:55 +08:00
var sessionIdleTracker = new GameIdleTracker ( 300000 ) ;
sessionIdleTracker . IsIdle . BindValueChanged ( idle = >
2021-04-16 17:53:27 +08:00
{
2021-04-19 10:30:55 +08:00
if ( idle . NewValue )
2021-12-21 14:34:32 +08:00
SessionStatics . ResetAfterInactivity ( ) ;
2021-04-16 17:53:27 +08:00
} ) ;
2019-05-13 16:10:25 +08:00
2021-04-19 13:06:26 +08:00
Add ( sessionIdleTracker ) ;
2021-04-19 10:30:55 +08:00
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
} ,
2021-08-07 03:36:40 +08:00
ScreenOffsetContainer = new Container
2019-01-04 12:29:37 +08:00
{
RelativeSizeAxes = Axes . Both ,
2019-01-31 17:25:25 +08:00
Children = new Drawable [ ]
{
2021-12-13 11:48:15 +08:00
ScreenContainer = new ScalingContainer ( ScalingMode . ExcludeOverlays )
2019-06-25 16:16:38 +08:00
{
2021-04-30 12:03:54 +08:00
RelativeSizeAxes = Axes . Both ,
Anchor = Anchor . Centre ,
Origin = Anchor . Centre ,
Children = new Drawable [ ]
2019-06-25 16:17:29 +08:00
{
2021-04-30 12:03:54 +08:00
receptor = new BackButton . Receptor ( ) ,
ScreenStack = new OsuScreenStack { RelativeSizeAxes = Axes . Both } ,
BackButton = new BackButton ( receptor )
{
Anchor = Anchor . BottomLeft ,
Origin = Anchor . BottomLeft ,
Action = ( ) = >
{
2021-06-08 16:38:12 +08:00
if ( ! ( ScreenStack . CurrentScreen is IOsuScreen currentScreen ) )
return ;
2021-04-30 12:03:54 +08:00
2021-06-08 17:39:52 +08:00
if ( ! ( ( Drawable ) currentScreen ) . IsLoaded | | ( currentScreen . AllowBackButton & & ! currentScreen . OnBackButton ( ) ) )
2021-04-30 12:03:54 +08:00
ScreenStack . Exit ( ) ;
}
} ,
logoContainer = new Container { RelativeSizeAxes = Axes . Both } ,
2019-06-25 16:17:29 +08:00
}
2019-06-25 16:16:38 +08:00
} ,
2019-01-31 17:25:25 +08:00
}
2019-01-04 12:29:37 +08:00
} ,
2021-08-29 11:13:01 +08:00
overlayOffsetContainer = new Container
{
RelativeSizeAxes = Axes . Both ,
Children = new Drawable [ ]
{
overlayContent = new Container { RelativeSizeAxes = Axes . Both } ,
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 } ,
2020-08-16 20:22:39 +08:00
idleTracker ,
2020-10-07 15:41:47 +08:00
new ConfineMouseTracker ( )
2018-04-13 17:19:50 +08:00
} ) ;
2019-07-29 13:30:46 +08:00
ScreenStack . ScreenPushed + = screenPushed ;
ScreenStack . ScreenExited + = screenExited ;
2019-01-23 19:52:00 +08:00
2022-01-16 22:20:22 +08:00
if ( ! args ? . Any ( a = > a = = @"--no-version-overlay" ) ? ? true )
loadComponentSingleFile ( versionManager = new VersionManager { Depth = int . MinValue } , ScreenContainer . Add ) ;
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-07-31 15:03:05 +08:00
ScreenStack . Push ( CreateLoader ( ) . With ( l = > l . RelativeSizeAxes = Axes . Both ) ) ;
2019-03-12 15:03:25 +08:00
} ) ;
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-11-08 22:04:18 +08:00
} , topMostOverlayContent . Add ) ;
2018-04-13 17:19:50 +08:00
2019-09-15 23:15:52 +08:00
loadComponentSingleFile ( volume = new VolumeOverlay ( ) , leftFloatingOverlayContent . Add , true ) ;
2019-09-15 22:31:40 +08:00
2020-11-11 12:51:20 +08:00
var onScreenDisplay = new OnScreenDisplay ( ) ;
onScreenDisplay . BeginTracking ( this , frameworkConfig ) ;
onScreenDisplay . BeginTracking ( this , LocalConfig ) ;
2018-04-13 20:13:09 +08:00
2020-11-11 12:51:20 +08:00
loadComponentSingleFile ( onScreenDisplay , Add , true ) ;
2019-08-13 13:29:58 +08:00
2022-04-18 18:59:57 +08:00
loadComponentSingleFile < INotificationOverlay > ( Notifications . With ( d = >
2019-03-29 13:53:40 +08:00
{
2020-07-19 10:37:38 +08:00
d . Anchor = Anchor . TopRight ;
d . Origin = Anchor . TopRight ;
} ) , rightFloatingOverlayContent . Add , true ) ;
2019-03-29 13:53:40 +08:00
2020-09-09 14:39:15 +08:00
loadComponentSingleFile ( new CollectionManager ( Storage )
{
2021-08-07 03:36:40 +08:00
PostNotification = n = > Notifications . Post ( n ) ,
2020-09-09 14:39:15 +08:00
} , Add , true ) ;
2021-11-25 16:12:15 +08:00
loadComponentSingleFile ( legacyImportManager , Add ) ;
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
2020-05-08 07:09:16 +08:00
// dependency on notification overlay, dependent by settings overlay
2020-05-08 05:04:18 +08:00
loadComponentSingleFile ( CreateUpdateManager ( ) , Add , true ) ;
2020-05-05 09:31:11 +08:00
// overlay elements
2020-09-07 20:08:48 +08:00
loadComponentSingleFile ( new ManageCollectionsDialog ( ) , overlayContent . Add , true ) ;
2020-04-21 15:00:00 +08:00
loadComponentSingleFile ( beatmapListing = new BeatmapListingOverlay ( ) , overlayContent . Add , true ) ;
2020-04-16 17:05:51 +08:00
loadComponentSingleFile ( dashboard = new DashboardOverlay ( ) , overlayContent . Add , true ) ;
2020-07-16 19:48:40 +08:00
loadComponentSingleFile ( news = new NewsOverlay ( ) , overlayContent . Add , true ) ;
2020-02-13 19:26:35 +08:00
var rankingsOverlay = loadComponentSingleFile ( new RankingsOverlay ( ) , overlayContent . Add , true ) ;
2019-05-13 16:10:25 +08:00
loadComponentSingleFile ( channelManager = new ChannelManager ( ) , AddInternal , true ) ;
loadComponentSingleFile ( chatOverlay = new ChatOverlay ( ) , overlayContent . Add , true ) ;
2021-06-11 15:28:53 +08:00
loadComponentSingleFile ( new MessageNotifier ( ) , AddInternal , true ) ;
2021-08-29 11:13:01 +08:00
loadComponentSingleFile ( Settings = new SettingsOverlay ( ) , leftFloatingOverlayContent . Add , true ) ;
2021-10-12 10:41:59 +08:00
loadComponentSingleFile ( changelogOverlay = 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 ) ;
2021-04-22 17:16:12 +08:00
loadComponentSingleFile ( wikiOverlay = new WikiOverlay ( ) , overlayContent . Add , true ) ;
2021-12-13 11:48:15 +08:00
loadComponentSingleFile ( skinEditor = new SkinEditorOverlay ( ScreenContainer ) , overlayContent . Add , true ) ;
2019-05-13 16:10:25 +08:00
2019-11-12 14:03:58 +08:00
loadComponentSingleFile ( new LoginOverlay
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
2020-08-06 16:17:24 +08:00
loadComponentSingleFile ( new NowPlayingOverlay
2018-04-13 17:19:50 +08:00
{
Anchor = Anchor . TopRight ,
Origin = Anchor . TopRight ,
2019-11-08 22:04:18 +08:00
} , rightFloatingOverlayContent . Add , true ) ;
2018-04-13 17:19:50 +08:00
2019-05-13 16:10:25 +08:00
loadComponentSingleFile ( new AccountCreationOverlay ( ) , topMostOverlayContent . Add , true ) ;
2022-04-18 17:36:22 +08:00
loadComponentSingleFile < IDialogOverlay > ( new DialogOverlay ( ) , topMostOverlayContent . Add , true ) ;
2018-12-06 10:55:58 +08:00
2021-06-16 19:53:48 +08:00
loadComponentSingleFile ( CreateHighPerformanceSession ( ) , Add ) ;
2018-12-06 10:55:58 +08:00
2022-01-13 16:31:49 +08:00
chatOverlay . State . BindValueChanged ( _ = > updateChatPollRate ( ) ) ;
// Multiplayer modes need to increase poll rate temporarily.
API . Activity . BindValueChanged ( _ = > updateChatPollRate ( ) , true ) ;
void updateChatPollRate ( )
{
channelManager . HighPollRate . Value =
chatOverlay . State . Value = = Visibility . Visible
| | API . Activity . Value is UserActivity . InLobby
2022-02-25 15:03:46 +08:00
| | API . Activity . Value is UserActivity . InMultiplayerGame
| | API . Activity . Value is UserActivity . SpectatingMultiplayerGame ;
2022-01-13 16:31:49 +08:00
}
2018-12-10 20:08:14 +08:00
2021-06-16 10:48:41 +08:00
Add ( difficultyRecommender ) ;
2018-10-24 04:03:00 +08:00
Add ( externalLinkOpener = new ExternalLinkOpener ( ) ) ;
2020-09-08 17:10:14 +08:00
Add ( new MusicKeyBindingHandler ( ) ) ;
2018-10-24 04:03:00 +08:00
2019-11-12 14:03:58 +08:00
// side overlays which cancel each other.
2021-08-07 03:36:40 +08:00
var singleDisplaySideOverlays = new OverlayContainer [ ] { Settings , Notifications } ;
2018-06-06 15:17:51 +08:00
2019-11-12 14:04:51 +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-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
{
2020-08-11 22:04:00 +08:00
if ( state . NewValue ! = Visibility . Hidden )
showOverlayAboveOthers ( overlay , informationalOverlays ) ;
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.
2021-04-22 17:16:12 +08:00
var singleDisplayOverlays = new OverlayContainer [ ] { chatOverlay , news , dashboard , beatmapListing , changelogOverlay , rankingsOverlay , wikiOverlay } ;
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 ( ) ) ;
2020-08-11 22:04:00 +08:00
if ( state . NewValue ! = Visibility . Hidden )
showOverlayAboveOthers ( overlay , singleDisplayOverlays ) ;
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
} ;
2021-10-13 11:18:56 +08:00
// Importantly, this should be run after binding PostNotification to the import handlers so they can present the import after game startup.
handleStartupImport ( ) ;
}
private void handleStartupImport ( )
{
if ( args ? . Length > 0 )
{
2021-10-27 12:04:41 +08:00
string [ ] paths = args . Where ( a = > ! a . StartsWith ( '-' ) ) . ToArray ( ) ;
2022-02-18 15:06:38 +08:00
2021-10-13 11:18:56 +08:00
if ( paths . Length > 0 )
2022-02-18 15:06:38 +08:00
{
string firstPath = paths . First ( ) ;
if ( firstPath . StartsWith ( OSU_PROTOCOL , StringComparison . Ordinal ) )
{
2022-02-23 16:02:39 +08:00
HandleLink ( firstPath ) ;
2022-02-18 15:06:38 +08:00
}
else
{
Task . Run ( ( ) = > Import ( paths ) ) ;
}
}
}
}
2020-08-11 22:04:00 +08:00
private void showOverlayAboveOthers ( OverlayContainer overlay , OverlayContainer [ ] otherOverlays )
2018-12-26 17:39:57 +08:00
{
2020-08-11 22:04:00 +08:00
otherOverlays . Where ( o = > o ! = overlay ) . ForEach ( o = > o . Hide ( ) ) ;
2018-12-26 17:39:57 +08:00
2021-03-19 19:09:12 +08:00
// Partially visible so leave it at the current depth.
if ( overlay . IsPresent )
2021-03-19 15:47:39 +08:00
return ;
2019-01-23 19:37:56 +08:00
2021-03-19 19:09:12 +08:00
// Show above all other overlays.
if ( overlay . IsLoaded )
2020-08-11 22:04:00 +08:00
overlayContent . ChangeChildDepth ( overlay , ( float ) - Clock . CurrentTime ) ;
2021-03-19 19:09:12 +08:00
else
overlay . Depth = ( float ) - Clock . CurrentTime ;
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
2019-07-26 12:48:29 +08:00
const double debounce = 60000 ;
2018-04-13 17:19:50 +08:00
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 )
{
2021-08-07 03:36:40 +08:00
Schedule ( ( ) = > Notifications . Post ( new SimpleErrorNotification
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 ,
2020-07-24 13:10:05 +08:00
Text = entry . Message . Truncate ( 256 ) + ( 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 )
{
2021-10-27 12:04:41 +08:00
string logFile = $@"{entry.Target.ToString().ToLowerInvariant()}.log" ;
2021-10-17 18:17:38 +08:00
2021-08-07 03:36:40 +08:00
Schedule ( ( ) = > Notifications . Post ( new SimpleNotification
2018-06-21 13:50:42 +08:00
{
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 = ( ) = >
{
2021-10-24 18:59:19 +08:00
Storage . GetStorageForDirectory ( @"logs" ) . PresentFileExternally ( logFile ) ;
2018-04-13 17:19:50 +08:00
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 ;
2020-05-12 11:49:35 +08:00
/// <summary>
2020-05-13 10:09:17 +08:00
/// Queues loading the provided component in sequential fashion.
/// This operation is limited to a single thread to avoid saturating all cores.
2020-05-12 11:49:35 +08:00
/// </summary>
2020-05-13 10:09:17 +08:00
/// <param name="component">The component to load.</param>
/// <param name="loadCompleteAction">An action to invoke on load completion (generally to add the component to the hierarchy).</param>
2020-05-12 11:49:35 +08:00
/// <param name="cache">Whether to cache the component as type <typeparamref name="T"/> into the game dependencies before any scheduling.</param>
2022-04-18 17:36:22 +08:00
private T loadComponentSingleFile < T > ( T component , Action < Drawable > loadCompleteAction , bool cache = false )
where T : class
2018-04-13 17:19:50 +08:00
{
2019-05-13 16:10:25 +08:00
if ( cache )
2020-05-13 10:09:17 +08:00
dependencies . CacheAs ( component ) ;
2019-05-13 16:10:25 +08:00
2022-04-18 19:04:19 +08:00
var drawableComponent = component as Drawable ? ? throw new ArgumentException ( $"Component must be a {nameof(Drawable)}" , nameof ( component ) ) ;
2022-04-18 17:36:22 +08:00
2021-09-01 05:29:16 +08:00
if ( component is OsuFocusedOverlayContainer overlay )
focusedOverlays . Add ( overlay ) ;
2019-11-12 14:03:58 +08:00
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 ;
2020-05-05 09:31:11 +08:00
// chain with existing load stream
2018-08-29 14:11:02 +08:00
asyncLoadStream = Task . Run ( async ( ) = >
2018-08-20 13:42:37 +08:00
{
2018-08-29 14:11:02 +08:00
if ( previousLoadStream ! = null )
2021-03-08 11:57:16 +08:00
await previousLoadStream . ConfigureAwait ( false ) ;
2018-08-29 14:11:02 +08:00
try
2018-08-20 15:06:12 +08:00
{
2021-07-22 13:45:56 +08:00
Logger . Log ( $"Loading {component}..." ) ;
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 ;
2022-04-18 17:36:22 +08:00
var del = new ScheduledDelegate ( ( ) = > task = LoadComponentAsync ( drawableComponent , loadCompleteAction ) ) ;
2019-02-28 16:17:51 +08:00
Scheduler . Add ( del ) ;
// The delegate won't complete if OsuGame has been disposed in the meantime
while ( ! IsDisposed & & ! del . Completed )
2021-03-08 11:57:16 +08:00
await Task . Delay ( 10 ) . ConfigureAwait ( false ) ;
2019-02-28 16:17:51 +08:00
// 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 ) ;
2021-03-08 11:57:16 +08:00
await task . ConfigureAwait ( false ) ;
2019-02-28 16:17:51 +08:00
2021-07-22 13:45:56 +08:00
Logger . Log ( $"Loaded {component}!" ) ;
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
2020-05-13 10:09:17 +08:00
return component ;
2020-03-02 17:56:09 +08:00
}
2021-09-16 17:26:12 +08:00
public bool OnPressed ( KeyBindingPressEvent < GlobalAction > e )
2018-04-13 17:19:50 +08:00
{
2021-11-18 11:35:47 +08:00
if ( e . Repeat )
return false ;
2019-01-23 19:52:00 +08:00
if ( introScreen = = null ) return false ;
2018-04-13 17:19:50 +08:00
2021-09-16 17:26:12 +08:00
switch ( e . Action )
2018-04-13 17:19:50 +08:00
{
2022-03-21 15:52:06 +08:00
case GlobalAction . ToggleSkinEditor :
skinEditor . ToggleVisibility ( ) ;
return true ;
2018-04-13 17:19:50 +08:00
case GlobalAction . ResetInputSettings :
2021-03-12 17:44:10 +08:00
Host . ResetInputHandlers ( ) ;
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-05-02 18:42:03 +08:00
case GlobalAction . ToggleGameplayMouseButtons :
2021-03-15 13:47:05 +08:00
var mouseDisableButtons = LocalConfig . GetBindable < bool > ( OsuSetting . MouseDisableButtons ) ;
mouseDisableButtons . Value = ! mouseDisableButtons . Value ;
2018-04-13 17:19:50 +08:00
return true ;
2019-04-01 11:16:05 +08:00
2020-11-11 12:05:03 +08:00
case GlobalAction . RandomSkin :
2022-04-01 13:22:26 +08:00
// Don't allow random skin selection while in the skin editor.
// This is mainly to stop many "osu! default (modified)" skins being created via the SkinManager.EnsureMutableSkin() path.
// If people want this to work we can potentially avoid selecting default skins when the editor is open, or allow a maximum of one mutable skin somehow.
if ( skinEditor . State . Value = = Visibility . Visible )
return false ;
2020-11-11 12:05:03 +08:00
SkinManager . SelectRandomSkin ( ) ;
2018-05-02 18:37:47 +08:00
return true ;
2018-04-13 17:19:50 +08:00
}
return false ;
}
2021-10-11 03:19:56 +08:00
public override bool OnPressed ( KeyBindingPressEvent < PlatformAction > e )
{
2021-10-28 12:09:03 +08:00
const float adjustment_increment = 0.05f ;
2021-10-11 03:19:56 +08:00
switch ( e . Action )
{
case PlatformAction . ZoomIn :
2021-10-28 12:09:03 +08:00
uiScale . Value + = adjustment_increment ;
2021-10-11 03:19:56 +08:00
return true ;
case PlatformAction . ZoomOut :
2021-10-28 12:09:03 +08:00
uiScale . Value - = adjustment_increment ;
2021-10-11 03:19:56 +08:00
return true ;
case PlatformAction . ZoomDefault :
2021-10-28 12:09:03 +08:00
uiScale . SetDefault ( ) ;
2021-10-11 03:19:56 +08:00
return true ;
}
return base . OnPressed ( e ) ;
}
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
2021-09-16 17:26:12 +08:00
public void OnReleased ( KeyBindingReleaseEvent < GlobalAction > e )
2020-01-22 12:22:34 +08:00
{
}
2018-04-13 17:19:50 +08:00
2019-01-24 19:13:29 +08:00
protected override bool OnExiting ( )
{
2019-07-29 13:30:46 +08:00
if ( ScreenStack . CurrentScreen is Loader )
2019-01-24 19:13:29 +08:00
return false ;
2020-05-09 18:11:51 +08:00
if ( introScreen ? . DidLoadMenu = = true & & ! ( ScreenStack . CurrentScreen is IntroScreen ) )
2019-01-24 19:13:29 +08:00
{
Scheduler . Add ( introScreen . MakeCurrent ) ;
return true ;
}
return base . OnExiting ( ) ;
}
2018-04-13 17:19:50 +08:00
protected override void UpdateAfterChildren ( )
{
base . UpdateAfterChildren ( ) ;
2021-08-29 11:13:01 +08:00
ScreenOffsetContainer . Padding = new MarginPadding { Top = toolbarOffset } ;
overlayOffsetContainer . Padding = new MarginPadding { Top = toolbarOffset } ;
2018-04-13 17:19:50 +08:00
2021-10-27 12:04:41 +08:00
float horizontalOffset = 0f ;
2021-08-08 00:33:22 +08:00
2021-08-21 20:39:57 +08:00
// Content.ToLocalSpace() is used instead of this.ToLocalSpace() to correctly calculate the offset with scaling modes active.
// Content is a child of a scaling container with ScalingMode.Everything set, while the game itself is never scaled.
// this avoids a visible jump in the positioning of the screen offset container.
2021-08-12 19:14:54 +08:00
if ( Settings . IsLoaded & & Settings . IsPresent )
2021-08-21 09:15:54 +08:00
horizontalOffset + = Content . ToLocalSpace ( Settings . ScreenSpaceDrawQuad . TopRight ) . X * SIDE_OVERLAY_OFFSET_RATIO ;
2021-08-12 19:14:54 +08:00
if ( Notifications . IsLoaded & & Notifications . IsPresent )
2021-08-21 09:15:54 +08:00
horizontalOffset + = ( Content . ToLocalSpace ( Notifications . ScreenSpaceDrawQuad . TopLeft ) . X - Content . DrawWidth ) * SIDE_OVERLAY_OFFSET_RATIO ;
2021-08-12 17:14:39 +08:00
ScreenOffsetContainer . X = horizontalOffset ;
2021-08-06 23:38:48 +08:00
2019-07-29 13:30:46 +08:00
MenuCursorContainer . CanShowCursor = ( ScreenStack . CurrentScreen as IOsuScreen ) ? . CursorVisible ? ? false ;
2018-04-13 17:19:50 +08:00
}
2022-03-11 18:54:13 +08:00
private void screenChanged ( IScreen current , IScreen newScreen )
2018-04-13 17:19:50 +08:00
{
2019-01-23 19:52:00 +08:00
switch ( newScreen )
{
2019-07-09 16:59:40 +08:00
case IntroScreen intro :
2019-01-23 19:52:00 +08:00
introScreen = intro ;
2022-01-16 02:42:38 +08:00
versionManager ? . Show ( ) ;
2019-01-23 19:52:00 +08:00
break ;
2019-04-01 11:16:05 +08:00
2019-01-23 19:52:00 +08:00
case MainMenu menu :
menuScreen = menu ;
2022-01-16 02:42:38 +08:00
versionManager ? . Show ( ) ;
break ;
default :
versionManager ? . Hide ( ) ;
2019-01-23 19:52:00 +08:00
break ;
}
2019-01-28 14:41:54 +08:00
2020-10-08 17:29:19 +08:00
// reset on screen change for sanity.
LocalUserPlaying . Value = false ;
2020-08-28 01:29:18 +08:00
if ( current is IOsuScreen currentOsuScreen )
2020-11-08 19:29:52 +08:00
{
2020-08-28 01:29:18 +08:00
OverlayActivationMode . UnbindFrom ( currentOsuScreen . OverlayActivationMode ) ;
2020-11-08 19:29:52 +08:00
API . Activity . UnbindFrom ( currentOsuScreen . Activity ) ;
}
2020-08-28 01:29:18 +08:00
2019-01-28 14:41:54 +08:00
if ( newScreen is IOsuScreen newOsuScreen )
{
2020-08-28 01:29:18 +08:00
OverlayActivationMode . BindTo ( newOsuScreen . OverlayActivationMode ) ;
2020-12-18 14:16:36 +08:00
API . Activity . BindTo ( newOsuScreen . Activity ) ;
2019-01-28 14:41:54 +08:00
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-07-29 13:30:46 +08:00
BackButton . Show ( ) ;
2019-06-25 17:30:43 +08:00
else
2019-07-29 13:30:46 +08:00
BackButton . Hide ( ) ;
2019-01-28 14:41:54 +08:00
}
2022-03-11 22:08:40 +08:00
2022-03-15 17:10:30 +08:00
skinEditor . SetTarget ( ( OsuScreen ) newScreen ) ;
2018-12-27 18:18:27 +08:00
}
2022-03-11 18:54:13 +08:00
private void screenPushed ( IScreen lastScreen , IScreen newScreen ) = > screenChanged ( lastScreen , 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
{
2022-03-11 18:54:13 +08:00
screenChanged ( lastScreen , newScreen ) ;
2018-04-13 17:19:50 +08:00
if ( newScreen = = null )
Exit ( ) ;
}
2021-08-17 15:13:45 +08:00
IBindable < bool > ILocalUserPlayInfo . IsPlaying = > LocalUserPlaying ;
2018-04-13 17:19:50 +08:00
}
}