1
0
mirror of https://github.com/ppy/osu.git synced 2026-06-03 00:20:23 +08:00
Files
osu-lazer/osu.Game/Screens/Menu/MainMenu.cs
T
Dan Balasescu 0988552567 Implement ranked play (#36819)
I don't really have much to say here. Instead, I'll give a brief history
rundown that lists many pages of documentation you can read, if
interested.

- Started off as BTMC + Happy24 (Vivi)'s ["The
Vision"](https://docs.google.com/document/d/1p1IpPmd2RICp8G4OqkCSs7u8Ug8FbFv8qqP0mfSrHf0/edit?tab=t.0#heading=h.fol093d9f9xi)
- Initial
[designs](https://www.figma.com/design/f5qqC57t9jFlgpzhRqUNVX/The-Vision?node-id=0-1&p=f)
were led by Vivi.
- Designs
[morphed](https://www.figma.com/design/vtFmLrXKvWNyYiRjTezFTM/Untitled--Copy-?node-id=0-1&p=f)
during development into what's currently present, led by @minetoblend.
- There is some more ongoing work creating a [game design
document](https://docs.google.com/document/d/1iffJFCsIBfYF0D4ogItSBEj6YBmbp-rdCpItAeaJiTA/edit?tab=t.0).

**tl;dr:** Create something with the mechanics of a trading card game
within osu!. The name of this is "ranked play".

---

To be frank, a lot of stuff is missing here. Some of it I don't want to
mention, because the point of this exercise is to get the system into
the hands of players, gather feedback especially around mechanics, and
discuss any further direction with the team.

I am expecting a blanket approval on all of the new code, with
particular attention to changes in existing components that I'll point
out in a self review.

There is also some [ongoing
work](https://github.com/smoogipoo/osu/pulls) that may arrive in this
branch prior to being merged.

---------

Co-authored-by: maarvin <minetoblend@gmail.com>
Co-authored-by: Marvin <m.schuerz@hautzy.com>
Co-authored-by: Jamie Taylor <me@nekodex.net>
Co-authored-by: ArijanJ <arijanj@proton.me>
Co-authored-by: Dean Herbert <pe@ppy.sh>
Co-authored-by: Tim Oliver <git@tim.dev>
Co-authored-by: Joseph Madamba <madamba.joehu@outlook.com>
Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
Co-authored-by: nil <25884226+voidstar0@users.noreply.github.com>
Co-authored-by: Ботников Максим <mr.botnikoff@ya.ru>
Co-authored-by: Denis Titovets <den232titovets@yandex.ru>
Co-authored-by: Michael Middlezong <119022671+mmiddlezong@users.noreply.github.com>
Co-authored-by: SupDos <6813986+SupDos@users.noreply.github.com>
Co-authored-by: failaip12 <86018517+failaip12@users.noreply.github.com>
2026-03-07 02:30:50 +09:00

512 lines
18 KiB
C#

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings;
using osu.Game.IO;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.Matchmaking;
using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
using osu.Game.Overlays.SkinEditor;
using osu.Game.Overlays.Volume;
using osu.Game.Rulesets;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit;
using osu.Game.Screens.OnlinePlay.DailyChallenge;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Screens.Select;
using osu.Game.Seasonal;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Menu
{
public partial class MainMenu : OsuScreen, IHandlePresentBeatmap, IKeyBindingHandler<GlobalAction>, ISamplePlaybackDisabler
{
public const float FADE_IN_DURATION = 300;
public const float FADE_OUT_DURATION = 400;
public override bool HideOverlaysOnEnter => Buttons == null || Buttons.State == ButtonSystemState.Initial;
public override bool AllowUserExit => false;
public override bool AllowExternalScreenChange => true;
public override bool? AllowGlobalTrackControl => true;
private MenuSideFlashes sideFlashes;
protected ButtonSystem Buttons;
[Resolved]
private GameHost host { get; set; }
[Resolved]
private INotificationOverlay notifications { get; set; }
[Resolved]
private MusicController musicController { get; set; }
[Resolved]
private IAPIProvider api { get; set; }
[Resolved]
private Storage storage { get; set; }
[Resolved(canBeNull: true)]
private LoginOverlay login { get; set; }
[Resolved(canBeNull: true)]
private IDialogOverlay dialogOverlay { get; set; }
// used to stop kiai fountain samples when navigating to other screens
IBindable<bool> ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled;
private readonly Bindable<bool> samplePlaybackDisabled = new Bindable<bool>();
protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault();
protected override bool PlayExitSound => false;
private Bindable<double> holdDelay;
private Bindable<bool> loginDisplayed;
private Bindable<bool> showMobileDisclaimer;
private HoldToExitGameOverlay holdToExitGameOverlay;
private bool exitConfirmedViaDialog;
private bool exitConfirmedViaHoldOrClick;
private ParallaxContainer buttonsContainer;
private SongTicker songTicker;
private Container logoTarget;
private OnlineMenuBanner onlineMenuBanner;
private MenuTipDisplay menuTipDisplay;
private FillFlowContainer bottomElementsFlow;
private SupporterDisplay supporterDisplay;
private Sample reappearSampleSwoosh;
[Resolved(canBeNull: true)]
private SkinEditorOverlay skinEditor { get; set; }
[CanBeNull]
private IDisposable logoProxy;
[BackgroundDependencyLoader(true)]
private void load(BeatmapListingOverlay beatmapListing, SettingsOverlay settings, OsuConfigManager config, SessionStatics statics, AudioManager audio)
{
holdDelay = config.GetBindable<double>(OsuSetting.UIHoldActivationDelay);
loginDisplayed = statics.GetBindable<bool>(Static.LoginOverlayDisplayed);
showMobileDisclaimer = config.GetBindable<bool>(OsuSetting.ShowMobileDisclaimer);
if (host.CanExit)
{
AddInternal(holdToExitGameOverlay = new HoldToExitGameOverlay
{
Action = () =>
{
exitConfirmedViaHoldOrClick = holdDelay.Value > 0;
this.Exit();
}
});
}
AddRangeInternal(new[]
{
SeasonalUIConfig.ENABLED ? new MainMenuSeasonalLighting() : Empty(),
new GlobalScrollAdjustsVolume(),
buttonsContainer = new ParallaxContainer
{
ParallaxAmount = 0.01f,
Children = new Drawable[]
{
Buttons = new ButtonSystem
{
OnEditBeatmap = () =>
{
Beatmap.SetDefault();
this.Push(new EditorLoader());
},
OnEditSkin = () =>
{
skinEditor?.Show();
},
OnSolo = loadSongSelect,
OnMultiplayer = () => this.Push(new Multiplayer()),
OnQuickPlay = loadQuickPlay,
OnRankedPlay = loadRankedPlay,
OnPlaylists = () => this.Push(new Playlists()),
OnDailyChallenge = room =>
{
if (statics.Get<bool>(Static.DailyChallengeIntroPlayed))
this.Push(new DailyChallenge(room));
else
this.Push(new DailyChallengeIntro(room));
},
OnExit = e =>
{
exitConfirmedViaHoldOrClick = e is MouseEvent;
this.Exit();
}
}
}
},
logoTarget = new Container { RelativeSizeAxes = Axes.Both, },
sideFlashes = SeasonalUIConfig.ENABLED ? new SeasonalMenuSideFlashes() : new MenuSideFlashes(),
songTicker = new SongTicker
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Margin = new MarginPadding { Right = 15, Top = 5 }
},
// For now, this is too much alongside the seasonal lighting.
SeasonalUIConfig.ENABLED ? Empty() : new KiaiMenuFountains(),
bottomElementsFlow = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Spacing = new Vector2(5),
Children = new Drawable[]
{
menuTipDisplay = new MenuTipDisplay
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
onlineMenuBanner = new OnlineMenuBanner
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
}
}
},
supporterDisplay = new SupporterDisplay
{
Margin = new MarginPadding(5),
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
},
holdToExitGameOverlay?.CreateProxy() ?? Empty()
});
float baseDim = SeasonalUIConfig.ENABLED ? 0.84f : 1;
Buttons.StateChanged += state =>
{
switch (state)
{
case ButtonSystemState.Initial:
case ButtonSystemState.Exit:
ApplyToBackground(b => b.FadeColour(OsuColour.Gray(baseDim), 500, Easing.OutSine));
onlineMenuBanner.State.Value = Visibility.Hidden;
break;
default:
ApplyToBackground(b => b.FadeColour(OsuColour.Gray(baseDim * 0.8f), 500, Easing.OutSine));
onlineMenuBanner.State.Value = Visibility.Visible;
break;
}
};
Buttons.OnSettings = () => settings?.ToggleVisibility();
Buttons.OnBeatmapListing = () => beatmapListing?.ToggleVisibility();
reappearSampleSwoosh = audio.Samples.Get(@"Menu/reappear-swoosh");
}
protected override void LoadComplete()
{
base.LoadComplete();
GetContainingInputManager();
}
public void ReturnToOsuLogo() => Buttons.State = ButtonSystemState.Initial;
public override void OnEntering(ScreenTransitionEvent e)
{
base.OnEntering(e);
Buttons.FadeInFromZero(500);
if (e.Last is IntroScreen && musicController.TrackLoaded)
{
var track = musicController.CurrentTrack;
// presume the track is the current beatmap's track. not sure how correct this assumption is but it has worked until now.
if (!track.IsRunning)
{
Beatmap.Value.PrepareTrackForPreview(false);
track.Restart();
}
}
if (storage is OsuStorage osuStorage && osuStorage.Error != OsuStorageError.None)
dialogOverlay?.Push(new StorageErrorDialog(osuStorage, osuStorage.Error));
}
[CanBeNull]
private ScheduledDelegate mobileDisclaimerSchedule;
protected override void LogoArriving(OsuLogo logo, bool resuming)
{
base.LogoArriving(logo, resuming);
Buttons.SetOsuLogo(logo);
logo.FadeColour(Color4.White, 100, Easing.OutQuint);
logo.FadeIn(100, Easing.OutQuint);
logoProxy = logo.ProxyToContainer(logoTarget);
if (resuming)
{
Buttons.State = ButtonSystemState.TopLevel;
this.FadeIn(FADE_IN_DURATION, Easing.OutQuint);
buttonsContainer.MoveTo(new Vector2(0, 0), FADE_IN_DURATION, Easing.OutQuint);
sideFlashes.Delay(FADE_IN_DURATION).FadeIn(64, Easing.InQuint);
}
else
{
// copy out old action to avoid accidentally capturing logo.Action in closure, causing a self-reference loop.
var previousAction = logo.Action;
// we want to hook into logo.Action to display certain overlays, but also preserve the return value of the old action.
// therefore pass the old action to displayLogin, so that it can return that value.
// this ensures that the OsuLogo sample does not play when it is not desired.
logo.Action = () => onLogoClick(previousAction);
}
}
private bool onLogoClick(Func<bool> originalAction)
{
if (showMobileDisclaimer.Value)
{
mobileDisclaimerSchedule?.Cancel();
mobileDisclaimerSchedule = Scheduler.AddDelayed(() =>
{
dialogOverlay.Push(new MobileDisclaimerDialog(() =>
{
showMobileDisclaimer.Value = false;
displayLoginIfApplicable();
}));
}, 500);
}
else
displayLoginIfApplicable();
return originalAction.Invoke();
}
private void displayLoginIfApplicable()
{
if (loginDisplayed.Value) return;
if (!api.IsLoggedIn || api.State.Value == APIState.RequiresSecondFactorAuth)
{
Scheduler.AddDelayed(() => login?.Show(), 500);
loginDisplayed.Value = true;
}
}
protected override void LogoSuspending(OsuLogo logo)
{
var seq = logo.FadeOut(300, Easing.InSine)
.ScaleTo(0.2f, 300, Easing.InSine);
logoProxy?.Dispose();
logoProxy = null;
seq.OnComplete(_ => Buttons.SetOsuLogo(null));
seq.OnAbort(_ => Buttons.SetOsuLogo(null));
}
protected override void LogoExiting(OsuLogo logo)
{
base.LogoExiting(logo);
logoProxy?.Dispose();
logoProxy = null;
}
public override void OnSuspending(ScreenTransitionEvent e)
{
base.OnSuspending(e);
Buttons.State = ButtonSystemState.EnteringMode;
this.FadeOut(FADE_OUT_DURATION, Easing.InSine);
buttonsContainer.MoveTo(new Vector2(-800, 0), FADE_OUT_DURATION, Easing.InSine);
sideFlashes.FadeOut(64, Easing.OutQuint);
bottomElementsFlow
.ScaleTo(0.9f, 1000, Easing.OutQuint)
.FadeOut(500, Easing.OutQuint);
supporterDisplay
.FadeOut(500, Easing.OutQuint);
samplePlaybackDisabled.Value = true;
}
public override void OnResuming(ScreenTransitionEvent e)
{
base.OnResuming(e);
// Ensures any playing `ButtonSystem` samples are stopped when returning to MainMenu (as to not overlap with the 'back' sample)
Buttons.StopSamplePlayback();
reappearSampleSwoosh?.Play();
ApplyToBackground(b => (b as BackgroundScreenDefault)?.Next());
musicController.EnsurePlayingSomething();
// Cycle tip on resuming
menuTipDisplay.ShowNextTip();
bottomElementsFlow
.ScaleTo(1, 1000, Easing.OutQuint)
.FadeIn(1000, Easing.OutQuint);
samplePlaybackDisabled.Value = false;
}
public override bool OnExiting(ScreenExitEvent e)
{
bool requiresConfirmation =
// we need to have a dialog overlay to confirm in the first place.
dialogOverlay != null
// if the dialog has already displayed and been accepted by the user, we are good.
&& !exitConfirmedViaDialog
// Only require confirmation if there is either an ongoing operation or the user exited via a non-hold escape press.
&& (notifications.HasOngoingOperations || !exitConfirmedViaHoldOrClick);
if (requiresConfirmation)
{
if (dialogOverlay.CurrentDialog is ConfirmExitDialog exitDialog)
{
if (exitDialog.Buttons.OfType<PopupDialogOkButton>().FirstOrDefault() != null)
exitDialog.PerformOkAction();
else
exitDialog.Flash();
}
else
{
dialogOverlay.Push(new ConfirmExitDialog(() =>
{
exitConfirmedViaDialog = true;
this.Exit();
}, () =>
{
holdToExitGameOverlay.Abort();
}));
}
return true;
}
Buttons.State = ButtonSystemState.Exit;
OverlayActivationMode.Value = OverlayActivation.Disabled;
songTicker.Hide();
this.FadeOut(3000);
bottomElementsFlow
.FadeOut(500, Easing.OutQuint);
supporterDisplay
.FadeOut(500, Easing.OutQuint);
return base.OnExiting(e);
}
public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset)
{
Logger.Log($"{nameof(MainMenu)} completing {nameof(PresentBeatmap)} with beatmap {beatmap} ruleset {ruleset}");
Beatmap.Value = beatmap;
Ruleset.Value = ruleset;
Schedule(loadSongSelect);
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat)
return false;
switch (e.Action)
{
case GlobalAction.Back:
// In the case of a host being able to exit, the back action is handled by ExitConfirmOverlay.
Debug.Assert(!host.CanExit);
return host.SuspendToBackground();
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
private void loadSongSelect() => this.Push(new SoloSongSelect());
private void loadQuickPlay() => this.Push(new OnlinePlay.Matchmaking.Intro.ScreenIntro(MatchmakingPoolType.QuickPlay));
private void loadRankedPlay() => this.Push(new OnlinePlay.Matchmaking.Intro.ScreenIntro(MatchmakingPoolType.RankedPlay));
private partial class MobileDisclaimerDialog : PopupDialog
{
public MobileDisclaimerDialog(Action confirmed)
{
HeaderText = ButtonSystemStrings.MobileDisclaimerHeader;
BodyText = ButtonSystemStrings.MobileDisclaimerBody;
Icon = FontAwesome.Solid.SmileBeam;
Buttons = new PopupDialogButton[]
{
new PopupDialogOkButton
{
Text = "Understood",
Action = confirmed,
},
};
}
}
}
}