Merge branch 'master' of https://github.com/ppy/osu into present-recommended

 Conflicts:
	osu.Game/OsuGameBase.cs
	osu.Game/Screens/Select/DifficultyRecommender.cs
	osu.Game/Screens/Select/SongSelect.cs
This commit is contained in:
Endrik Tombak
2020-11-21 13:59:59 +02:00
1470 changed files with 49711 additions and 13757 deletions

View File

@ -18,6 +18,7 @@ using osu.Game.Screens.Menu;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Humanizer;
using JetBrains.Annotations;
using osu.Framework.Audio;
using osu.Framework.Bindables;
@ -30,23 +31,27 @@ using osu.Framework.Input.Events;
using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input;
using osu.Game.Overlays.Notifications;
using osu.Game.Screens.Play;
using osu.Game.Input.Bindings;
using osu.Game.Online.Chat;
using osu.Game.Overlays.Music;
using osu.Game.Skinning;
using osuTK.Graphics;
using osu.Game.Overlays.Volume;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
using osu.Game.Updater;
using osu.Game.Utils;
using LogLevel = osu.Framework.Logging.LogLevel;
using osu.Game.Users;
namespace osu.Game
{
@ -62,14 +67,15 @@ namespace osu.Game
private ChannelManager channelManager;
private NotificationOverlay notifications;
private NowPlayingOverlay nowPlaying;
[NotNull]
private readonly NotificationOverlay notifications = new NotificationOverlay();
private BeatmapListingOverlay beatmapListing;
private DashboardOverlay dashboard;
private NewsOverlay news;
private UserProfileOverlay userProfile;
private BeatmapSetOverlay beatmapSetOverlay;
@ -81,11 +87,23 @@ namespace osu.Game
public virtual Storage GetStorageForStableInstall() => null;
public float ToolbarOffset => Toolbar.Position.Y + Toolbar.DrawHeight;
public float ToolbarOffset => (Toolbar?.Position.Y ?? 0) + (Toolbar?.DrawHeight ?? 0);
private IdleTracker idleTracker;
public readonly Bindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>();
/// <summary>
/// Whether overlays should be able to be opened game-wide. Value is sourced from the current active screen.
/// </summary>
public readonly IBindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>();
/// <summary>
/// Whether the local user is currently interacting with the game in a way that should not be interrupted.
/// </summary>
/// <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();
protected OsuScreenStack ScreenStack;
@ -164,7 +182,7 @@ namespace osu.Game
if (args?.Length > 0)
{
var paths = args.Where(a => !a.StartsWith(@"-")).ToArray();
var paths = args.Where(a => !a.StartsWith('-')).ToArray();
if (paths.Length > 0)
Task.Run(() => Import(paths));
}
@ -249,7 +267,7 @@ namespace osu.Game
case LinkAction.OpenEditorTimestamp:
case LinkAction.JoinMultiplayerMatch:
case LinkAction.Spectate:
waitForReady(() => notifications, _ => notifications?.Post(new SimpleNotification
waitForReady(() => notifications, _ => notifications.Post(new SimpleNotification
{
Text = @"This link type is not yet supported!",
Icon = FontAwesome.Solid.LifeRing,
@ -261,7 +279,7 @@ namespace osu.Game
break;
case LinkAction.OpenUserProfile:
if (long.TryParse(link.Argument, out long userId))
if (int.TryParse(link.Argument, out int userId))
ShowUser(userId);
break;
@ -272,7 +290,7 @@ namespace osu.Game
public void OpenUrlExternally(string url) => waitForReady(() => externalLinkOpener, _ =>
{
if (url.StartsWith("/"))
if (url.StartsWith('/'))
url = $"{API.Endpoint}{url}";
externalLinkOpener.OpenUrlExternally(url);
@ -304,7 +322,7 @@ namespace osu.Game
/// Show a user's profile as an overlay.
/// </summary>
/// <param name="userId">The user to display.</param>
public void ShowUser(long userId) => waitForReady(() => userProfile, _ => userProfile.ShowUser(userId));
public void ShowUser(int userId) => waitForReady(() => userProfile, _ => userProfile.ShowUser(userId));
/// <summary>
/// Show a beatmap's set as an overlay, displaying the given beatmap.
@ -370,7 +388,7 @@ namespace osu.Game
/// Present a score's replay immediately.
/// The user should have already requested this interactively.
/// </summary>
public void PresentScore(ScoreInfo score)
public void PresentScore(ScoreInfo score, ScorePresentType presentType = ScorePresentType.Results)
{
// 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.
@ -402,9 +420,19 @@ namespace osu.Game
PerformFromScreen(screen =>
{
Ruleset.Value = databasedScore.ScoreInfo.Ruleset;
Beatmap.Value = BeatmapManager.GetWorkingBeatmap(databasedBeatmap);
screen.Push(new ReplayPlayerLoader(databasedScore));
switch (presentType)
{
case ScorePresentType.Gameplay:
screen.Push(new ReplayPlayerLoader(databasedScore));
break;
case ScorePresentType.Results:
screen.Push(new SoloResultsScreen(databasedScore.ScoreInfo));
break;
}
}, validScreens: new[] { typeof(PlaySongSelect) });
}
@ -422,23 +450,7 @@ namespace osu.Game
updateModDefaults();
var newBeatmap = beatmap.NewValue;
if (newBeatmap != null)
{
newBeatmap.Track.Completed += () => Scheduler.AddOnce(() => trackCompleted(newBeatmap));
newBeatmap.BeginAsyncLoad();
}
void trackCompleted(WorkingBeatmap b)
{
// the source of track completion is the audio thread, so the beatmap may have changed before firing.
if (Beatmap.Value != b)
return;
if (!Beatmap.Value.Track.Looping && !Beatmap.Disabled)
MusicController.NextTrack();
}
beatmap.NewValue?.BeginAsyncLoad();
}
private void modsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
@ -461,7 +473,7 @@ namespace osu.Game
#endregion
private ScheduledDelegate performFromMainMenuTask;
private PerformFromMenuRunner performFromMainMenuTask;
/// <summary>
/// Perform an action only after returning to a specific screen as indicated by <paramref name="validScreens"/>.
@ -472,34 +484,7 @@ namespace osu.Game
public void PerformFromScreen(Action<IScreen> action, IEnumerable<Type> validScreens = null)
{
performFromMainMenuTask?.Cancel();
validScreens ??= Enumerable.Empty<Type>();
validScreens = validScreens.Append(typeof(MainMenu));
CloseAllOverlays(false);
// we may already be at the target screen type.
if (validScreens.Contains(ScreenStack.CurrentScreen?.GetType()) && !Beatmap.Disabled)
{
action(ScreenStack.CurrentScreen);
return;
}
// find closest valid target
IScreen screen = ScreenStack.CurrentScreen;
while (screen != null)
{
if (validScreens.Contains(screen.GetType()))
{
screen.MakeCurrent();
break;
}
screen = screen.GetParentScreen();
}
performFromMainMenuTask = Schedule(() => PerformFromScreen(action, validScreens));
Add(performFromMainMenuTask = new PerformFromMenuRunner(action, validScreens, () => ScreenStack.CurrentScreen));
}
/// <summary>
@ -535,17 +520,31 @@ namespace osu.Game
MenuCursorContainer.CanShowCursor = menuScreen?.CursorVisible ?? false;
// todo: all archive managers should be able to be looped here.
SkinManager.PostNotification = n => notifications?.Post(n);
SkinManager.PostNotification = n => notifications.Post(n);
SkinManager.GetStableStorage = GetStorageForStableInstall;
BeatmapManager.PostNotification = n => notifications?.Post(n);
BeatmapManager.PostNotification = n => notifications.Post(n);
BeatmapManager.GetStableStorage = GetStorageForStableInstall;
BeatmapManager.PresentImport = items => PresentBeatmap(items.First());
ScoreManager.PostNotification = n => notifications?.Post(n);
ScoreManager.PostNotification = n => notifications.Post(n);
ScoreManager.GetStableStorage = GetStorageForStableInstall;
ScoreManager.PresentImport = items => PresentScore(items.First());
// 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.
LocalConfig.LookupSkinName = id => SkinManager.GetAllUsableSkins().FirstOrDefault(s => s.ID == id)?.ToString() ?? "Unknown";
LocalConfig.LookupKeyBindings = l =>
{
var combinations = KeyBindingStore.GetReadableKeyCombinationsFor(l).ToArray();
if (combinations.Length == 0)
return "none";
return string.Join(" or ", combinations);
};
Container logoContainer;
BackButton.Receptor receptor;
@ -572,7 +571,9 @@ namespace osu.Game
Origin = Anchor.BottomLeft,
Action = () =>
{
if ((ScreenStack.CurrentScreen as IOsuScreen)?.AllowBackButton == true)
var currentScreen = ScreenStack.CurrentScreen as IOsuScreen;
if (currentScreen?.AllowBackButton == true && !currentScreen.OnBackButton())
ScreenStack.Exit();
}
},
@ -583,7 +584,8 @@ namespace osu.Game
rightFloatingOverlayContent = new Container { RelativeSizeAxes = Axes.Both },
leftFloatingOverlayContent = new Container { RelativeSizeAxes = Axes.Both },
topMostOverlayContent = new Container { RelativeSizeAxes = Axes.Both },
idleTracker
idleTracker,
new ConfineMouseTracker()
});
ScreenStack.ScreenPushed += screenPushed;
@ -608,22 +610,36 @@ namespace osu.Game
loadComponentSingleFile(volume = new VolumeOverlay(), leftFloatingOverlayContent.Add, true);
loadComponentSingleFile(new OnScreenDisplay(), Add, true);
var onScreenDisplay = new OnScreenDisplay();
loadComponentSingleFile(MusicController = new MusicController(), Add, true);
onScreenDisplay.BeginTracking(this, frameworkConfig);
onScreenDisplay.BeginTracking(this, LocalConfig);
loadComponentSingleFile(notifications = new NotificationOverlay
loadComponentSingleFile(onScreenDisplay, Add, true);
loadComponentSingleFile(notifications.With(d =>
{
GetToolbarHeight = () => ToolbarOffset,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
}, rightFloatingOverlayContent.Add, true);
d.GetToolbarHeight = () => ToolbarOffset;
d.Anchor = Anchor.TopRight;
d.Origin = Anchor.TopRight;
}), rightFloatingOverlayContent.Add, true);
loadComponentSingleFile(new CollectionManager(Storage)
{
PostNotification = n => notifications.Post(n),
GetStableStorage = GetStorageForStableInstall
}, Add, true);
loadComponentSingleFile(screenshotManager, Add);
// dependency on notification overlay, dependent by settings overlay
loadComponentSingleFile(CreateUpdateManager(), Add, true);
// overlay elements
loadComponentSingleFile(new ManageCollectionsDialog(), overlayContent.Add, true);
loadComponentSingleFile(beatmapListing = new BeatmapListingOverlay(), overlayContent.Add, true);
loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true);
loadComponentSingleFile(news = new NewsOverlay(), overlayContent.Add, true);
var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true);
loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true);
loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true);
@ -639,7 +655,7 @@ namespace osu.Game
Origin = Anchor.TopRight,
}, rightFloatingOverlayContent.Add, true);
loadComponentSingleFile(nowPlaying = new NowPlayingOverlay
loadComponentSingleFile(new NowPlayingOverlay
{
GetToolbarHeight = () => ToolbarOffset,
Anchor = Anchor.TopRight,
@ -648,12 +664,11 @@ namespace osu.Game
loadComponentSingleFile(new AccountCreationOverlay(), topMostOverlayContent.Add, true);
loadComponentSingleFile(new DialogOverlay(), topMostOverlayContent.Add, true);
loadComponentSingleFile(externalLinkOpener = new ExternalLinkOpener(), topMostOverlayContent.Add);
chatOverlay.State.ValueChanged += state => channelManager.HighPollRate.Value = state.NewValue == Visibility.Visible;
Add(externalLinkOpener = new ExternalLinkOpener());
Add(CreateUpdateManager()); // dependency on notification overlay
Add(new MusicKeyBindingHandler());
// side overlays which cancel each other.
var singleDisplaySideOverlays = new OverlayContainer[] { Settings, notifications };
@ -675,14 +690,13 @@ namespace osu.Game
{
overlay.State.ValueChanged += state =>
{
if (state.NewValue == Visibility.Hidden) return;
informationalOverlays.Where(o => o != overlay).ForEach(o => o.Hide());
if (state.NewValue != Visibility.Hidden)
showOverlayAboveOthers(overlay, informationalOverlays);
};
}
// ensure only one of these overlays are open at once.
var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, dashboard, beatmapListing, changelogOverlay, rankingsOverlay };
var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, news, dashboard, beatmapListing, changelogOverlay, rankingsOverlay };
foreach (var overlay in singleDisplayOverlays)
{
@ -691,9 +705,8 @@ namespace osu.Game
// informational overlays should be dismissed on a show or hide of a full overlay.
informationalOverlays.ForEach(o => o.Hide());
if (state.NewValue == Visibility.Hidden) return;
singleDisplayOverlays.Where(o => o != overlay).ForEach(o => o.Hide());
if (state.NewValue != Visibility.Hidden)
showOverlayAboveOthers(overlay, singleDisplayOverlays);
};
}
@ -707,9 +720,9 @@ namespace osu.Game
float offset = 0;
if (Settings.State.Value == Visibility.Visible)
offset += ToolbarButton.WIDTH / 2;
offset += Toolbar.HEIGHT / 2;
if (notifications.State.Value == Visibility.Visible)
offset -= ToolbarButton.WIDTH / 2;
offset -= Toolbar.HEIGHT / 2;
screenContainer.MoveToX(offset, SettingsPanel.TRANSITION_LENGTH, Easing.OutQuint);
}
@ -718,22 +731,13 @@ namespace osu.Game
notifications.State.ValueChanged += _ => updateScreenOffset();
}
public class GameIdleTracker : IdleTracker
private void showOverlayAboveOthers(OverlayContainer overlay, OverlayContainer[] otherOverlays)
{
private InputManager inputManager;
otherOverlays.Where(o => o != overlay).ForEach(o => o.Hide());
public GameIdleTracker(int time)
: base(time)
{
}
protected override void LoadComplete()
{
base.LoadComplete();
inputManager = GetContainingInputManager();
}
protected override bool AllowIdle => inputManager.FocusedDrawable == null;
// show above others if not visible at all, else leave at current depth.
if (!overlay.IsPresent)
overlayContent.ChangeChildDepth(overlay, (float)-Clock.CurrentTime);
}
private void forwardLoggedErrorsToNotifications()
@ -753,7 +757,7 @@ namespace osu.Game
Schedule(() => notifications.Post(new SimpleNotification
{
Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb,
Text = entry.Message + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty),
Text = entry.Message.Truncate(256) + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty),
}));
}
else if (recentLogCount == short_term_display_limit)
@ -764,7 +768,7 @@ namespace osu.Game
Text = "Subsequent messages have been logged. Click to view log files.",
Activated = () =>
{
Host.Storage.GetStorageForDirectory("logs").OpenInNativeExplorer();
Storage.GetStorageForDirectory("logs").OpenInNativeExplorer();
return true;
}
}));
@ -852,18 +856,6 @@ namespace osu.Game
switch (action)
{
case GlobalAction.ToggleNowPlaying:
nowPlaying.ToggleVisibility();
return true;
case GlobalAction.ToggleChat:
chatOverlay.ToggleVisibility();
return true;
case GlobalAction.ToggleSocial:
dashboard.ToggleVisibility();
return true;
case GlobalAction.ResetInputSettings:
var sensitivity = frameworkConfig.GetBindable<double>(FrameworkSetting.CursorSensitivity);
@ -879,17 +871,13 @@ namespace osu.Game
Toolbar.ToggleVisibility();
return true;
case GlobalAction.ToggleSettings:
Settings.ToggleVisibility();
return true;
case GlobalAction.ToggleDirect:
beatmapListing.ToggleVisibility();
return true;
case GlobalAction.ToggleGameplayMouseButtons:
LocalConfig.Set(OsuSetting.MouseDisableButtons, !LocalConfig.Get<bool>(OsuSetting.MouseDisableButtons));
return true;
case GlobalAction.RandomSkin:
SkinManager.SelectRandomSkin();
return true;
}
return false;
@ -926,8 +914,6 @@ namespace osu.Game
private ScalingContainer screenContainer;
protected MusicController MusicController { get; private set; }
protected override bool OnExiting()
{
if (ScreenStack.CurrentScreen is Loader)
@ -977,9 +963,19 @@ namespace osu.Game
break;
}
// reset on screen change for sanity.
LocalUserPlaying.Value = false;
if (current is IOsuScreen currentOsuScreen)
{
OverlayActivationMode.UnbindFrom(currentOsuScreen.OverlayActivationMode);
API.Activity.UnbindFrom(currentOsuScreen.Activity);
}
if (newScreen is IOsuScreen newOsuScreen)
{
OverlayActivationMode.Value = newOsuScreen.InitialOverlayActivationMode;
OverlayActivationMode.BindTo(newOsuScreen.OverlayActivationMode);
((IBindable<UserActivity>)API.Activity).BindTo(newOsuScreen.Activity);
MusicController.AllowRateAdjustments = newOsuScreen.AllowRateAdjustments;