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
2022-06-17 15:37:17 +08:00
#nullable disable
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 ;
2023-06-18 00:57:08 +08:00
using System.IO ;
2018-04-13 17:19:50 +08:00
using System.Linq ;
2024-06-26 13:47:58 +08:00
using System.Reflection ;
2018-04-13 17:19:50 +08:00
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 ;
2022-06-03 14:37:17 +08:00
using osu.Framework ;
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 ;
2022-12-12 17:27:14 +08:00
using osu.Framework.Extensions ;
2018-07-13 19:32:22 +08:00
using osu.Framework.Extensions.IEnumerableExtensions ;
2022-05-12 11:06:51 +08:00
using osu.Framework.Extensions.TypeExtensions ;
2021-11-22 16:40:43 +08:00
using osu.Framework.Graphics ;
using osu.Framework.Graphics.Containers ;
2024-05-16 12:20:55 +08:00
using osu.Framework.Graphics.Cursor ;
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 ;
2022-09-16 21:17:24 +08:00
using osu.Framework.Input.Handlers.Tablet ;
2022-06-28 15:29:19 +08:00
using osu.Framework.Localisation ;
2021-11-22 16:40:43 +08:00
using osu.Framework.Logging ;
2023-06-18 00:57:08 +08:00
using osu.Framework.Platform ;
2021-11-22 16:40:43 +08:00
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 ;
2022-06-21 02:04:21 +08:00
using osu.Game.Online ;
2018-09-14 11:06:04 +08:00
using osu.Game.Online.Chat ;
2023-10-03 07:31:30 +08:00
using osu.Game.Online.Rooms ;
2021-11-22 16:40:43 +08:00
using osu.Game.Overlays ;
2022-12-22 03:02:04 +08:00
using osu.Game.Overlays.BeatmapListing ;
2024-06-29 15:17:40 +08:00
using osu.Game.Overlays.Mods ;
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 ;
2023-01-26 17:21:04 +08:00
using osu.Game.Overlays.SkinEditor ;
2021-11-22 16:40:43 +08:00
using osu.Game.Overlays.Toolbar ;
2018-04-13 17:19:50 +08:00
using osu.Game.Overlays.Volume ;
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 ;
2023-11-04 09:01:18 +08:00
using osu.Game.Screens.Edit ;
2024-05-16 12:20:55 +08:00
using osu.Game.Screens.Footer ;
2021-11-22 16:40:43 +08:00
using osu.Game.Screens.Menu ;
2023-10-03 07:31:30 +08:00
using osu.Game.Screens.OnlinePlay.Multiplayer ;
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 ;
2023-02-14 07:29:50 +08:00
using osu.Game.Skinning ;
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 ;
2022-05-11 11:55:15 +08:00
using Sentry ;
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-10-12 22:50:31 +08:00
[Cached(typeof(OsuGame))]
2022-11-24 13:32:20 +08:00
public partial class OsuGame : OsuGameBase , IKeyBindingHandler < GlobalAction > , ILocalUserPlayInfo , IPerformFromScreenRunner , IOverlayManager , ILinkHandler
2018-04-13 17:19:50 +08:00
{
2024-01-21 09:55:52 +08:00
#if DEBUG
2024-07-12 20:25:21 +08:00
// Different port allows running release and debug builds alongside each other.
2024-01-21 09:55:52 +08:00
public const int IPC_PORT = 44824 ;
#else
2024-01-18 18:45:35 +08:00
public const int IPC_PORT = 44823 ;
2024-01-21 09:55:52 +08:00
#endif
2024-01-18 18:45:35 +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
2024-05-16 12:27:54 +08:00
/// <summary>
/// A common shear factor applied to most components of the game.
/// </summary>
public const float SHEAR = 0.2f ;
2023-06-21 17:27:29 +08:00
public Toolbar Toolbar { get ; private set ; }
2018-04-13 17:19:50 +08:00
2022-05-30 16:54:09 +08:00
private ChatOverlay chatOverlay ;
2018-11-23 10:00:17 +08:00
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 ;
2024-06-29 15:17:40 +08:00
private Container footerBasedOverlayContent ;
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 ;
2022-10-12 22:50:31 +08:00
/// <summary>
/// Whether the user is currently in an idle state.
/// </summary>
public IBindable < bool > IsIdle = > idleTracker . IsIdle ;
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 ;
2024-05-16 12:20:55 +08:00
protected ScreenFooter ScreenFooter ;
2019-07-29 13:30:46 +08:00
2020-05-12 11:49:35 +08:00
protected SettingsOverlay Settings ;
2019-07-31 18:47:41 +08:00
2022-06-15 08:34:08 +08:00
protected FirstRunSetupOverlay FirstRunOverlay { get ; private set ; }
2022-04-06 16:42:10 +08:00
2022-07-20 20:05:20 +08:00
private FPSCounter fpsCounter ;
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
2022-10-28 12:52:45 +08:00
private Bindable < bool > applySafeAreaConsiderations ;
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 > ( ) ;
2022-05-05 03:53:04 +08:00
private readonly List < OverlayContainer > externalOverlays = new List < OverlayContainer > ( ) ;
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
2022-09-22 18:17:01 +08:00
forwardGeneralLogsToNotifications ( ) ;
2022-09-16 21:17:24 +08:00
forwardTabletLogsToNotifications ( ) ;
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
}
2022-05-05 21:47:10 +08:00
#region IOverlayManager
IBindable < OverlayActivation > IOverlayManager . OverlayActivationMode = > OverlayActivationMode ;
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
2022-05-05 21:47:10 +08:00
IDisposable IOverlayManager . RegisterBlockingOverlay ( OverlayContainer overlayContainer )
2022-05-05 03:53:04 +08:00
{
if ( overlayContainer . Parent ! = null )
2022-05-05 21:47:10 +08:00
throw new ArgumentException ( $@"Overlays registered via {nameof(IOverlayManager.RegisterBlockingOverlay)} should not be added to the scene graph." ) ;
2022-05-05 03:53:04 +08:00
if ( externalOverlays . Contains ( overlayContainer ) )
2022-05-05 21:47:10 +08:00
throw new ArgumentException ( $@"{overlayContainer} has already been registered via {nameof(IOverlayManager.RegisterBlockingOverlay)} once." ) ;
2022-05-05 03:53:04 +08:00
externalOverlays . Add ( overlayContainer ) ;
2024-06-29 15:17:40 +08:00
if ( overlayContainer is ShearedOverlayContainer )
footerBasedOverlayContent . Add ( overlayContainer ) ;
else
overlayContent . Add ( overlayContainer ) ;
2022-05-08 04:50:10 +08:00
if ( overlayContainer is OsuFocusedOverlayContainer focusedOverlayContainer )
focusedOverlays . Add ( focusedOverlayContainer ) ;
2022-05-05 03:53:04 +08:00
return new InvokeOnDisposal ( ( ) = > unregisterBlockingOverlay ( overlayContainer ) ) ;
}
2022-05-05 21:47:10 +08:00
void IOverlayManager . ShowBlockingOverlay ( OverlayContainer overlay )
2019-03-01 11:20:31 +08:00
{
if ( ! visibleBlockingOverlays . Contains ( overlay ) )
visibleBlockingOverlays . Add ( overlay ) ;
updateBlockingOverlayFade ( ) ;
}
2022-05-05 21:47:10 +08:00
void IOverlayManager . HideBlockingOverlay ( 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
2022-05-05 03:53:04 +08:00
/// <summary>
/// Unregisters a blocking <see cref="OverlayContainer"/> that was not created by <see cref="OsuGame"/> itself.
/// </summary>
2022-06-28 00:19:20 +08:00
private void unregisterBlockingOverlay ( OverlayContainer overlayContainer ) = > Schedule ( ( ) = >
2022-05-05 03:53:04 +08:00
{
externalOverlays . Remove ( overlayContainer ) ;
2022-05-08 04:50:10 +08:00
if ( overlayContainer is OsuFocusedOverlayContainer focusedOverlayContainer )
focusedOverlays . Remove ( focusedOverlayContainer ) ;
2022-05-05 03:53:04 +08:00
overlayContainer . Expire ( ) ;
2022-06-28 00:19:20 +08:00
} ) ;
2022-05-05 03:53:04 +08:00
2022-05-05 21:47:10 +08:00
#endregion
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
{
2022-06-28 00:19:20 +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
}
2023-05-13 20:12:21 +08:00
protected override UserInputManager CreateUserInputManager ( )
{
var userInputManager = base . CreateUserInputManager ( ) ;
( userInputManager as OsuUserInputManager ) ? . LocalUserPlaying . BindTo ( LocalUserPlaying ) ;
return userInputManager ;
}
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
2023-06-18 00:57:08 +08:00
private readonly List < string > dragDropFiles = new List < string > ( ) ;
private ScheduledDelegate dragDropImportSchedule ;
public override void SetHost ( GameHost host )
{
base . SetHost ( host ) ;
2023-07-02 01:02:09 +08:00
if ( host . Window ! = null )
2023-06-18 00:57:08 +08:00
{
2023-07-02 01:02:09 +08:00
host . Window . DragDrop + = path = >
2023-06-18 00:57:08 +08:00
{
// on macOS/iOS, URL associations are handled via SDL_DROPFILE events.
if ( path . StartsWith ( OSU_PROTOCOL , StringComparison . Ordinal ) )
{
HandleLink ( path ) ;
return ;
}
lock ( dragDropFiles )
{
dragDropFiles . Add ( path ) ;
Logger . Log ( $@"Adding ""{Path.GetFileName(path)}"" for import" ) ;
// File drag drop operations can potentially trigger hundreds or thousands of these calls on some platforms.
// In order to avoid spawning multiple import tasks for a single drop operation, debounce a touch.
dragDropImportSchedule ? . Cancel ( ) ;
dragDropImportSchedule = Scheduler . AddDelayed ( handlePendingDragDropImports , 100 ) ;
}
} ;
}
}
private void handlePendingDragDropImports ( )
{
lock ( dragDropFiles )
{
Logger . Log ( $"Handling batch import of {dragDropFiles.Count} files" ) ;
string [ ] paths = dragDropFiles . ToArray ( ) ;
dragDropFiles . Clear ( ) ;
Task . Factory . StartNew ( ( ) = > Import ( paths ) , TaskCreationOptions . LongRunning ) ;
}
}
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
{
2022-05-10 13:25:10 +08:00
SentryLogger . AttachUser ( API . LocalUser ) ;
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
2022-10-28 12:12:18 +08:00
var preferredRuleset = 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
2021-11-23 15:04:55 +08:00
configSkin = LocalConfig . GetBindable < string > ( OsuSetting . Skin ) ;
2022-09-12 18:51:49 +08:00
// Transfer skin from config to realm instance once on startup.
SkinManager . SetSkinFromConfiguration ( configSkin . Value ) ;
2019-08-29 15:38:39 +08:00
2022-09-12 18:51:49 +08:00
// Transfer any runtime changes back to configuration file.
SkinManager . CurrentSkinInfo . ValueChanged + = skin = > configSkin . Value = skin . NewValue . ID . ToString ( ) ;
2018-04-13 17:19:50 +08:00
2023-01-10 00:10:20 +08:00
LocalUserPlaying . BindValueChanged ( p = >
{
BeatmapManager . PauseImports = p . NewValue ;
SkinManager . PauseImports = p . NewValue ;
ScoreManager . PauseImports = p . NewValue ;
} , true ) ;
2023-01-09 17:54:11 +08:00
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 ) ;
2022-10-28 12:52:45 +08:00
applySafeAreaConsiderations = LocalConfig . GetBindable < bool > ( OsuSetting . SafeAreaConsiderations ) ;
2022-11-01 15:31:09 +08:00
applySafeAreaConsiderations . BindValueChanged ( apply = > SafeAreaContainer . SafeAreaOverrideEdges = apply . NewValue ? SafeAreaOverrideEdges : Edges . All , 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
{
2022-12-19 15:41:04 +08:00
string argString = link . Argument . ToString ( ) ? ? string . Empty ;
2021-11-08 13:17:47 +08:00
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 :
2022-12-25 03:26:09 +08:00
if ( link . Argument is RomanisableString romanisable )
2022-12-26 08:03:44 +08:00
SearchBeatmapSet ( romanisable . GetPreferred ( Localisation . CurrentParameters . Value . PreferOriginalScript ) ) ;
2022-12-25 03:26:09 +08:00
else
SearchBeatmapSet ( argString ) ;
2020-01-30 12:30:25 +08:00
break ;
2022-12-19 14:52:21 +08:00
case LinkAction . FilterBeatmapSetGenre :
2022-12-22 03:02:04 +08:00
FilterBeatmapSetGenre ( ( SearchGenre ) link . Argument ) ;
2022-12-19 14:52:21 +08:00
break ;
case LinkAction . FilterBeatmapSetLanguage :
2022-12-22 03:02:04 +08:00
FilterBeatmapSetLanguage ( ( SearchLanguage ) link . Argument ) ;
2022-12-19 14:52:21 +08:00
break ;
2019-11-01 10:40:51 +08:00
case LinkAction . OpenEditorTimestamp :
2023-11-12 22:09:15 +08:00
HandleTimestamp ( argString ) ;
2023-11-04 09:01:18 +08:00
break ;
2019-11-01 10:40:51 +08:00
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
{
2023-06-28 02:56:35 +08:00
Text = NotificationsStrings . LinkTypeNotSupported ,
2019-11-01 10:22:32 +08:00
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 :
2023-06-24 23:11:34 +08:00
ShowUser ( ( IUser ) link . Argument ) ;
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
2024-05-30 15:39:53 +08:00
public void OpenUrlExternally ( string url , bool forceBypassExternalUrlWarning = false ) = > waitForReady ( ( ) = > externalLinkOpener , _ = >
2018-12-06 11:17:08 +08:00
{
2024-05-30 15:39:53 +08:00
bool isTrustedDomain ;
2020-10-16 17:27:02 +08:00
if ( url . StartsWith ( '/' ) )
2024-05-30 15:39:53 +08:00
{
url = $"{API.WebsiteRootUrl}{url}" ;
isTrustedDomain = true ;
}
else
{
2024-05-31 14:38:26 +08:00
isTrustedDomain = url . StartsWith ( API . WebsiteRootUrl , StringComparison . Ordinal ) ;
2024-05-30 15:39:53 +08:00
}
2018-12-06 11:17:08 +08:00
2022-12-12 17:27:14 +08:00
if ( ! url . CheckIsValidUrl ( ) )
{
Notifications . Post ( new SimpleErrorNotification
{
2023-06-28 02:56:35 +08:00
Text = NotificationsStrings . UnsupportedOrDangerousUrlProtocol ( url ) ,
2022-12-12 17:27:14 +08:00
} ) ;
return ;
}
2024-05-30 15:39:53 +08:00
externalLinkOpener . OpenUrlExternally ( url , forceBypassExternalUrlWarning | | isTrustedDomain ) ;
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 ) ) ;
2022-12-22 03:02:04 +08:00
public void FilterBeatmapSetGenre ( SearchGenre genre ) = > waitForReady ( ( ) = > beatmapListing , _ = > beatmapListing . ShowWithGenreFilter ( genre ) ) ;
2022-12-19 14:52:21 +08:00
2022-12-22 03:02:04 +08:00
public void FilterBeatmapSetLanguage ( SearchLanguage language ) = > waitForReady ( ( ) = > beatmapListing , _ = > beatmapListing . ShowWithLanguageFilter ( language ) ) ;
2022-12-19 14:52:21 +08:00
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 ) ) ;
2023-11-04 09:01:18 +08:00
/// <summary>
2023-11-20 20:02:12 +08:00
/// Seeks to the provided <paramref name="timestamp"/> if the editor is currently open.
/// Can also select objects as indicated by the <paramref name="timestamp"/> (depends on ruleset implementation).
2023-11-04 09:01:18 +08:00
/// </summary>
2023-11-12 22:09:15 +08:00
public void HandleTimestamp ( string timestamp )
2023-11-04 09:01:18 +08:00
{
if ( ScreenStack . CurrentScreen is not Editor editor )
{
2023-11-21 14:09:33 +08:00
Schedule ( ( ) = > Notifications . Post ( new SimpleErrorNotification
2023-11-07 08:36:58 +08:00
{
Icon = FontAwesome . Solid . ExclamationTriangle ,
2023-11-21 14:08:15 +08:00
Text = EditorStrings . MustBeInEditorToHandleLinks
2023-11-07 08:36:58 +08:00
} ) ) ;
2023-11-07 07:56:24 +08:00
return ;
}
2024-06-18 18:33:12 +08:00
editor . HandleTimestamp ( timestamp , notifyOnError : true ) ;
2023-11-04 09:01:18 +08:00
}
2023-02-14 07:29:50 +08:00
/// <summary>
2023-02-14 14:43:40 +08:00
/// Present a skin select immediately.
2023-02-14 07:29:50 +08:00
/// </summary>
2023-02-14 15:00:22 +08:00
/// <param name="skin">The skin to select.</param>
public void PresentSkin ( SkinInfo skin )
{
var databasedSkin = SkinManager . Query ( s = > s . ID = = skin . ID ) ;
if ( databasedSkin = = null )
{
Logger . Log ( "The requested skin could not be loaded." , LoggingTarget . Information ) ;
return ;
}
SkinManager . CurrentSkinInfo . Value = databasedSkin ;
}
2023-02-14 07:29:50 +08:00
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-06-27 17:41:27 +08:00
Logger . Log ( $"Beginning {nameof(PresentBeatmap)} with beatmap {beatmap}" ) ;
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 ( ) ) ;
2024-02-14 06:59:45 +08:00
if ( detachedSet . DeletePending )
{
Logger . Log ( "The requested beatmap has since been deleted." , LoggingTarget . Information ) ;
return ;
}
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
{
2022-11-25 18:47:55 +08:00
// Don't change the local ruleset if the user is on another ruleset and is showing converted beatmaps at song select.
// Eventually we probably want to check whether conversion is actually possible for the current ruleset.
bool requiresRulesetSwitch = ! selection . Ruleset . Equals ( Ruleset . Value )
& & ( selection . Ruleset . OnlineID > 0 | | ! LocalConfig . Get < bool > ( OsuSetting . ShowConvertedBeatmaps ) ) ;
if ( requiresRulesetSwitch )
{
Ruleset . Value = selection . Ruleset ;
Beatmap . Value = BeatmapManager . GetWorkingBeatmap ( selection ) ;
Logger . Log ( $"Completing {nameof(PresentBeatmap)} with beatmap {beatmap} ruleset {selection.Ruleset}" ) ;
}
else
{
Beatmap . Value = BeatmapManager . GetWorkingBeatmap ( selection ) ;
Logger . Log ( $"Completing {nameof(PresentBeatmap)} with beatmap {beatmap} (maintaining ruleset)" ) ;
}
2022-01-10 15:34:32 +08:00
}
2022-11-25 18:47:55 +08:00
} , validScreens : new [ ]
{
typeof ( SongSelect ) , typeof ( IHandlePresentBeatmap )
} ) ;
2018-07-11 00:32:10 +08:00
}
2023-10-03 07:31:30 +08:00
/// <summary>
/// Join a multiplayer match immediately.
/// </summary>
/// <param name="room">The room to join.</param>
/// <param name="password">The password to join the room, if any is given.</param>
public void PresentMultiplayerMatch ( Room room , string password )
{
PerformFromScreen ( screen = >
{
2023-10-12 16:42:02 +08:00
if ( ! ( screen is Multiplayer multiplayer ) )
screen . Push ( multiplayer = new Multiplayer ( ) ) ;
2023-10-03 07:31:30 +08:00
2023-10-12 16:42:02 +08:00
multiplayer . Join ( room , password ) ;
2023-10-03 07:31:30 +08:00
} ) ;
2023-10-12 16:42:02 +08:00
// TODO: We should really be able to use `validScreens: new[] { typeof(Multiplayer) }` here
// but `PerformFromScreen` doesn't understand nested stacks.
2023-10-03 07:31:30 +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
{
2022-06-27 17:41:27 +08:00
Logger . Log ( $"Beginning {nameof(PresentScore)} with score {score}" ) ;
2024-04-16 11:48:51 +08:00
var databasedScore = ScoreManager . GetScore ( score ) ;
2021-06-15 13:06:17 +08:00
2024-04-16 11:48:51 +08:00
if ( databasedScore = = null ) return ;
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
2024-04-23 02:15:50 +08:00
var databasedBeatmap = BeatmapManager . QueryBeatmap ( b = > b . ID = = databasedScore . ScoreInfo . 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 ;
}
2022-10-03 19:04:37 +08:00
// This should be able to be performed from song select, but that is disabled for now
// due to the weird decoupled ruleset logic (which can cause a crash in certain filter scenarios).
2022-10-10 15:20:17 +08:00
//
// As a special case, if the beatmap and ruleset already match, allow immediately displaying the score from song select.
// This is guaranteed to not crash, and feels better from a user's perspective (ie. if they are clicking a score in the
// song select leaderboard).
IEnumerable < Type > validScreens =
Beatmap . Value . BeatmapInfo . Equals ( databasedBeatmap ) & & Ruleset . Value . Equals ( databasedScore . ScoreInfo . Ruleset )
? new [ ] { typeof ( SongSelect ) }
: Array . Empty < Type > ( ) ;
2020-01-30 22:34:04 +08:00
PerformFromScreen ( screen = >
2019-02-25 11:58:58 +08:00
{
2022-10-04 17:21:29 +08:00
Logger . Log ( $"{nameof(PresentScore)} updating beatmap ({databasedBeatmap}) and ruleset ({databasedScore.ScoreInfo.Ruleset}) to match score" ) ;
2022-06-27 17:41:27 +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 :
2024-02-23 02:15:02 +08:00
screen . Push ( new SoloResultsScreen ( databasedScore . ScoreInfo ) ) ;
2020-06-15 19:23:35 +08:00
break ;
}
2022-10-10 15:20:17 +08:00
} , validScreens : validScreens ) ;
2019-02-25 11:58:58 +08:00
}
2022-12-13 20:03:25 +08:00
public override Task Import ( ImportTask [ ] imports , ImportParameters parameters = default )
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.
2022-12-13 20:03:25 +08:00
var importTask = new Task ( async ( ) = > await base . Import ( imports , parameters ) . 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 ( ) ;
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 ( ) ;
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
}
2022-06-19 11:34:14 +08:00
public override void AttemptExit ( )
2022-06-18 21:59:19 +08:00
{
2023-06-21 18:06:47 +08:00
// The main menu exit implementation gives the user a chance to interrupt the exit process if needed.
PerformFromScreen ( menu = > menu . Exit ( ) , new [ ] { typeof ( MainMenu ) } ) ;
2022-06-18 21:59:19 +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 ( )
2022-06-03 14:37:17 +08:00
{
return new Dictionary < FrameworkSetting , object >
2021-03-24 12:37:37 +08:00
{
2022-06-03 14:37:17 +08:00
// General expectation that osu! starts in fullscreen by default (also gives the most predictable performance).
// However, macOS is bound to have issues when using exclusive fullscreen as it takes full control away from OS, therefore borderless is default there.
2024-04-29 23:22:25 +08:00
{ FrameworkSetting . WindowMode , RuntimeInfo . OS = = RuntimeInfo . Platform . macOS ? WindowMode . Borderless : WindowMode . Fullscreen } ,
{ FrameworkSetting . VolumeUniversal , 0.6 } ,
{ FrameworkSetting . VolumeMusic , 0.6 } ,
{ FrameworkSetting . VolumeEffect , 0.6 } ,
2021-03-24 12:37:37 +08:00
} ;
2022-06-03 14:37:17 +08:00
}
2021-03-24 12:37:37 +08:00
2018-04-13 17:19:50 +08:00
protected override void LoadComplete ( )
{
base . LoadComplete ( ) ;
2024-06-26 13:47:58 +08:00
if ( RuntimeInfo . EntryAssembly . GetCustomAttribute < OfficialBuildAttribute > ( ) = = null )
Logger . Log ( NotificationsStrings . NotOfficialBuild . ToString ( ) ) ;
2022-12-27 03:36:39 +08:00
var languages = Enum . GetValues < Language > ( ) ;
2022-06-28 15:29:19 +08:00
var mappings = languages . Select ( language = >
2021-04-20 16:06:01 +08:00
{
2022-04-19 13:30:45 +08:00
#if DEBUG
2022-04-19 15:49:41 +08:00
if ( language = = Language . debug )
2022-06-28 15:29:19 +08:00
return new LocaleMapping ( "debug" , new DebugLocalisationStore ( ) ) ;
2022-04-19 13:30:45 +08:00
#endif
2021-10-27 12:04:41 +08:00
string cultureCode = language . ToCultureCode ( ) ;
2021-06-19 14:00:36 +08:00
try
{
2022-06-28 15:29:19 +08:00
return new LocaleMapping ( new ResourceManagerLocalisationStore ( cultureCode ) ) ;
2021-06-19 14:00:36 +08:00
}
catch ( Exception ex )
{
Logger . Error ( ex , $"Could not load localisations for language \" { cultureCode } \ "" ) ;
2022-06-28 15:29:19 +08:00
return null ;
2021-06-19 14:00:36 +08:00
}
2022-06-28 15:29:19 +08:00
} ) . Where ( m = > m ! = null ) ;
Localisation . AddLocaleMappings ( mappings ) ;
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
2022-07-26 13:11:52 +08:00
GlobalCursorDisplay . ShowCursor = menuScreen ? . CursorVisible ? ? false ;
2019-02-15 15:55:39 +08:00
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 ) ;
2023-02-14 15:00:22 +08:00
SkinManager . PresentImport = items = > PresentSkin ( items . First ( ) . Value ) ;
2018-04-13 17:19:50 +08:00
2021-08-07 03:36:40 +08:00
BeatmapManager . PostNotification = n = > Notifications . Post ( n ) ;
2022-06-20 17:21:37 +08:00
BeatmapManager . PresentImport = 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 ) ;
2022-06-20 17:21:37 +08:00
ScoreManager . PresentImport = items = > PresentScore ( items . First ( ) . Value ) ;
2018-04-13 17:19:50 +08:00
2022-09-15 19:54:06 +08:00
MultiplayerClient . PostNotification = n = > Notifications . Post ( n ) ;
2023-10-12 16:59:47 +08:00
MultiplayerClient . PresentMatch = PresentMultiplayerMatch ;
2022-09-15 19:54:06 +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
} ;
2024-05-16 12:20:55 +08:00
ScreenFooter . BackReceptor backReceptor ;
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
2024-07-12 20:25:21 +08:00
Container logoContainer ;
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
{
2024-05-16 12:20:55 +08:00
backReceptor = new ScreenFooter . BackReceptor ( ) ,
2021-04-30 12:03:54 +08:00
ScreenStack = new OsuScreenStack { RelativeSizeAxes = Axes . Both } ,
2024-05-16 12:20:55 +08:00
BackButton = new BackButton ( backReceptor )
2021-04-30 12:03:54 +08:00
{
Anchor = Anchor . BottomLeft ,
Origin = Anchor . BottomLeft ,
2024-05-16 12:20:55 +08:00
Action = ( ) = > ScreenFooter . OnBack ? . Invoke ( ) ,
} ,
2024-06-29 15:17:40 +08:00
logoContainer = new Container { RelativeSizeAxes = Axes . Both } ,
footerBasedOverlayContent = new Container
{
Depth = - 1 ,
RelativeSizeAxes = Axes . Both ,
} ,
2024-05-16 12:20:55 +08:00
new PopoverContainer
{
2024-06-29 15:17:40 +08:00
Depth = - 1 ,
2024-05-16 12:20:55 +08:00
RelativeSizeAxes = Axes . Both ,
Child = ScreenFooter = new ScreenFooter ( backReceptor )
2021-04-30 12:03:54 +08:00
{
2024-07-12 18:20:41 +08:00
RequestLogoInFront = inFront = > ScreenContainer . ChangeChildDepth ( logoContainer , inFront ? float . MinValue : 0 ) ,
2024-05-16 12:20:55 +08:00
OnBack = ( ) = >
{
if ( ! ( ScreenStack . CurrentScreen is IOsuScreen currentScreen ) )
return ;
if ( ! ( ( Drawable ) currentScreen ) . IsLoaded | | ( currentScreen . AllowBackButton & & ! currentScreen . OnBackButton ( ) ) )
ScreenStack . Exit ( ) ;
}
} ,
2021-04-30 12:03:54 +08:00
} ,
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 } ,
leftFloatingOverlayContent = new Container { RelativeSizeAxes = Axes . Both } ,
2022-08-30 20:04:28 +08:00
rightFloatingOverlayContent = new Container { RelativeSizeAxes = Axes . Both } ,
2021-08-29 11:13:01 +08:00
}
} ,
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
} ) ;
2024-05-16 12:20:55 +08:00
dependencies . Cache ( ScreenFooter ) ;
2019-07-29 13:30:46 +08:00
ScreenStack . ScreenPushed + = screenPushed ;
ScreenStack . ScreenExited + = screenExited ;
2019-01-23 19:52:00 +08:00
2022-07-20 20:05:20 +08:00
loadComponentSingleFile ( fpsCounter = new FPSCounter
2022-07-20 19:49:57 +08:00
{
Anchor = Anchor . BottomRight ,
Origin = Anchor . BottomRight ,
2022-07-20 22:59:09 +08:00
Margin = new MarginPadding ( 5 ) ,
2022-07-20 19:49:57 +08:00
} , topMostOverlayContent . Add ) ;
2023-12-29 18:14:28 +08:00
if ( ! IsDeployedBuild )
2023-12-28 06:07:17 +08:00
{
2024-06-29 15:17:40 +08:00
dependencies . Cache ( versionManager = new VersionManager ( ) ) ;
2023-12-28 06:07:17 +08:00
loadComponentSingleFile ( versionManager , ScreenContainer . Add ) ;
}
2022-01-16 22:20:22 +08:00
2023-07-14 18:19:14 +08:00
loadComponentSingleFile ( osuLogo , _ = >
2018-04-13 17:19:50 +08:00
{
2023-07-14 18:19:14 +08:00
osuLogo . SetupDefaultContainer ( logoContainer ) ;
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
2024-02-23 02:50:46 +08:00
loadComponentSingleFile ( new UserStatisticsWatcher ( ) , Add , true ) ;
2018-04-13 17:19:50 +08:00
loadComponentSingleFile ( Toolbar = new Toolbar
{
OnHome = delegate
{
2018-06-06 15:17:51 +08:00
CloseAllOverlays ( false ) ;
2022-09-09 13:52:46 +08:00
if ( menuScreen ? . GetChildScreen ( ) ! = null )
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
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
2024-06-29 15:17:40 +08:00
loadComponentSingleFile ( FirstRunOverlay = new FirstRunSetupOverlay ( ) , footerBasedOverlayContent . Add , true ) ;
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 ) ;
2023-01-12 03:02:06 +08:00
loadComponentSingleFile ( channelManager = new ChannelManager ( API ) , Add , true ) ;
2022-05-30 16:54:09 +08:00
loadComponentSingleFile ( chatOverlay = new ChatOverlay ( ) , overlayContent . Add , true ) ;
2023-01-12 03:02:06 +08:00
loadComponentSingleFile ( new MessageNotifier ( ) , Add , 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 ) ;
2024-02-20 23:16:30 +08:00
loadComponentSingleFile ( new MedalOverlay ( ) , topMostOverlayContent . Add ) ;
2018-12-06 10:55:58 +08:00
2023-07-26 15:07:45 +08:00
loadComponentSingleFile ( new BackgroundDataStoreProcessor ( ) , Add ) ;
2022-07-21 02:18:57 +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 ( ) ) ;
2023-11-21 13:39:33 +08:00
Add ( new OnlineStatusNotifier ( ( ) = > ScreenStack . CurrentScreen ) ) ;
2018-10-24 04:03:00 +08:00
2019-11-12 14:03:58 +08:00
// side overlays which cancel each other.
2022-06-15 08:34:08 +08:00
var singleDisplaySideOverlays = new OverlayContainer [ ] { Settings , Notifications , FirstRunOverlay } ;
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.
2024-06-29 15:17:40 +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
2022-11-08 13:38:02 +08:00
Settings . Hide ( ) ;
Notifications . Hide ( ) ;
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
}
2022-09-22 18:17:01 +08:00
private void forwardGeneralLogsToNotifications ( )
2018-04-13 17:19:50 +08:00
{
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 = >
{
2022-12-23 01:07:53 +08:00
if ( entry . Level < LogLevel . Important | | entry . Target > LoggingTarget . Database | | entry . Target = = null ) return ;
2022-12-19 15:41:04 +08:00
2024-03-01 12:34:21 +08:00
if ( entry . Exception is SentryOnlyDiagnosticsException )
return ;
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 )
{
2023-12-19 04:11:00 +08:00
string logFile = Logger . GetLogger ( entry . Target . Value ) . Filename ;
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 ,
2023-06-28 02:56:35 +08:00
Text = NotificationsStrings . SubsequentMessagesLogged ,
2018-04-13 17:19:50 +08:00
Activated = ( ) = >
{
2023-12-19 04:11:00 +08:00
Logger . Storage . 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
} ;
}
2022-09-16 21:17:24 +08:00
private void forwardTabletLogsToNotifications ( )
{
2022-09-22 17:37:21 +08:00
const string tablet_prefix = @"[Tablet] " ;
2023-06-30 12:46:02 +08:00
2022-09-16 21:17:24 +08:00
bool notifyOnWarning = true ;
2023-06-30 12:46:02 +08:00
bool notifyOnError = true ;
2022-09-16 21:17:24 +08:00
Logger . NewEntry + = entry = >
{
2022-09-22 18:21:27 +08:00
if ( entry . Level < LogLevel . Important | | entry . Target ! = LoggingTarget . Input | | ! entry . Message . StartsWith ( tablet_prefix , StringComparison . OrdinalIgnoreCase ) )
2022-09-16 21:17:24 +08:00
return ;
2022-09-22 17:37:21 +08:00
string message = entry . Message . Replace ( tablet_prefix , string . Empty ) ;
2022-09-16 21:17:24 +08:00
if ( entry . Level = = LogLevel . Error )
{
2023-06-30 12:46:02 +08:00
if ( ! notifyOnError )
return ;
notifyOnError = false ;
2023-06-01 14:16:47 +08:00
Schedule ( ( ) = >
2022-09-16 21:17:24 +08:00
{
2023-06-01 14:16:47 +08:00
Notifications . Post ( new SimpleNotification
{
2023-06-28 02:56:35 +08:00
Text = NotificationsStrings . TabletSupportDisabledDueToError ( message ) ,
2023-06-01 14:16:47 +08:00
Icon = FontAwesome . Solid . PenSquare ,
IconColour = Colours . RedDark ,
} ) ;
2023-06-02 07:48:47 +08:00
// We only have one tablet handler currently.
// The loop here is weakly guarding against a future where more than one is added.
// If this is ever the case, this logic needs adjustment as it should probably only
// disable the relevant tablet handler rather than all.
foreach ( var tabletHandler in Host . AvailableInputHandlers . OfType < ITabletHandler > ( ) )
2023-06-01 14:16:47 +08:00
tabletHandler . Enabled . Value = false ;
} ) ;
2022-09-16 21:17:24 +08:00
}
else if ( notifyOnWarning )
{
Schedule ( ( ) = > Notifications . Post ( new SimpleNotification
{
2023-06-28 02:56:35 +08:00
Text = NotificationsStrings . EncounteredTabletWarning ,
2022-09-16 21:17:24 +08:00
Icon = FontAwesome . Solid . PenSquare ,
IconColour = Colours . YellowDark ,
Activated = ( ) = >
{
OpenUrlExternally ( "https://opentabletdriver.net/Tablets" , true ) ;
return true ;
}
} ) ) ;
notifyOnWarning = false ;
}
} ;
Schedule ( ( ) = >
{
ITabletHandler tablet = Host . AvailableInputHandlers . OfType < ITabletHandler > ( ) . SingleOrDefault ( ) ;
2023-06-30 12:46:02 +08:00
tablet ? . Tablet . BindValueChanged ( _ = >
{
notifyOnWarning = true ;
notifyOnError = true ;
} , true ) ;
2022-09-16 21:17:24 +08:00
} ) ;
}
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-07-20 20:05:20 +08:00
case GlobalAction . ToggleFPSDisplay :
fpsCounter . ToggleVisibility ( ) ;
return true ;
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
2022-08-10 02:43:37 +08:00
case GlobalAction . ToggleProfile :
if ( userProfile . State . Value = = Visibility . Visible )
2022-08-11 01:31:58 +08:00
userProfile . Hide ( ) ;
else
2022-08-11 12:17:14 +08:00
ShowUser ( API . LocalUser . Value ) ;
return true ;
2022-08-10 02:43:37 +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 ;
2022-05-07 13:02:07 +08:00
overlayContent . X = horizontalOffset * 1.2f ;
2021-08-06 23:38:48 +08:00
2022-07-26 13:11:52 +08:00
GlobalCursorDisplay . ShowCursor = ( 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
{
2022-05-11 11:55:15 +08:00
SentrySdk . ConfigureScope ( scope = >
{
scope . Contexts [ @"screen stack" ] = new
{
2022-05-12 11:06:51 +08:00
Current = newScreen ? . GetType ( ) . ReadableName ( ) ,
Previous = current ? . GetType ( ) . ReadableName ( ) ,
2022-05-11 11:55:15 +08:00
} ;
2022-05-16 14:50:15 +08:00
scope . SetTag ( @"screen" , newScreen ? . GetType ( ) . ReadableName ( ) ? ? @"none" ) ;
2022-05-11 11:55:15 +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
2022-10-20 08:44:58 +08:00
GlobalCursorDisplay . MenuCursor . HideCursorOnNonMouseInput = newOsuScreen . HideMenuCursorOnNonMouseInput ;
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 ( ) ;
2024-05-16 12:20:55 +08:00
if ( newOsuScreen . ShowFooter )
{
BackButton . Hide ( ) ;
ScreenFooter . SetButtons ( newOsuScreen . CreateFooterButtons ( ) ) ;
ScreenFooter . Show ( ) ;
}
else
{
ScreenFooter . SetButtons ( Array . Empty < ScreenFooterButton > ( ) ) ;
ScreenFooter . 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
}
}