diff --git a/.gitignore b/.gitignore index 732b171f69..d122d25054 100644 --- a/.gitignore +++ b/.gitignore @@ -334,3 +334,5 @@ inspectcode # BenchmarkDotNet /BenchmarkDotNet.Artifacts + +*.GeneratedMSBuildEditorConfig.editorconfig diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt index e34626a59e..47839608c9 100644 --- a/CodeAnalysis/BannedSymbols.txt +++ b/CodeAnalysis/BannedSymbols.txt @@ -4,5 +4,6 @@ M:System.ValueType.Equals(System.Object)~System.Boolean;Don't use object.Equals( M:System.Nullable`1.Equals(System.Object)~System.Boolean;Use == instead. T:System.IComparable;Don't use non-generic IComparable. Use generic version instead. M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText. +M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900) T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods. T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal extension methods. diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index d03a764bda..ac1f11e09f 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -62,6 +62,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public abstract class DrawableCatchHitObject : DrawableHitObject { + protected override double InitialLifetimeOffset => HitObject.TimePreempt; + public virtual bool StaysOnPlate => HitObject.CanBePlated; public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale; diff --git a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs index 7efd832f62..1a80adb584 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Catch.Replays if (Position > lastCatchFrame.Position) lastCatchFrame.Actions.Add(CatchAction.MoveRight); else if (Position < lastCatchFrame.Position) - Actions.Add(CatchAction.MoveLeft); + lastCatchFrame.Actions.Add(CatchAction.MoveLeft); } } diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs index 4a3dc58604..e4dc261363 100644 --- a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs +++ b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Audio private readonly ControlPointInfo controlPoints; private readonly Dictionary mappings = new Dictionary(); - private IBindableList samplePoints; + private readonly IBindableList samplePoints = new BindableList(); public DrumSampleContainer(ControlPointInfo controlPoints) { @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.Audio [BackgroundDependencyLoader] private void load() { - samplePoints = controlPoints.SamplePoints.GetBoundCopy(); + samplePoints.BindTo(controlPoints.SamplePoints); samplePoints.BindCollectionChanged((_, __) => recreateMappings(), true); } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index da98a7a024..cf5f1b8818 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.Editing AddToggleStep("toggle y", state => selectionBox.CanScaleY = state); } - private void handleScale(Vector2 amount, Anchor reference) + private bool handleScale(Vector2 amount, Anchor reference) { if ((reference & Anchor.y1) == 0) { @@ -58,12 +58,15 @@ namespace osu.Game.Tests.Visual.Editing selectionArea.X += amount.X; selectionArea.Width += directionX * amount.X; } + + return true; } - private void handleRotation(float angle) + private bool handleRotation(float angle) { // kinda silly and wrong, but just showing that the drag handles work. selectionArea.Rotation += angle; + return true; } } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs index 082268d824..95d11d6909 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs @@ -7,13 +7,14 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; -using osu.Framework.Utils; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Graphics.Cursor; using osu.Game.Screens.Edit.Compose.Components.Timeline; using osuTK; using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Tests.Visual.Editing { @@ -88,6 +89,7 @@ namespace osu.Game.Tests.Visual.Editing // Scroll in at 0.25 AddStep("Move mouse to 0.25x", () => InputManager.MoveMouseTo(new Vector2(scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X, scrollQuad.Centre.Y))); + AddStep("Press alt down", () => InputManager.PressKey(Key.AltLeft)); AddStep("Scroll by 3", () => InputManager.ScrollBy(new Vector2(0, 3))); AddAssert("Box not at 0", () => !Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft)); AddAssert("Box 1/4 at 1/4", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.25f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X)); @@ -96,6 +98,25 @@ namespace osu.Game.Tests.Visual.Editing AddStep("Scroll by -3", () => InputManager.ScrollBy(new Vector2(0, -3))); AddAssert("Box at 0", () => Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft)); AddAssert("Box 1/4 at 1/4", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.25f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X)); + AddStep("Release alt", () => InputManager.ReleaseKey(Key.AltLeft)); + } + + [Test] + public void TestMouseZoomInThenScroll() + { + reset(); + + // Scroll in at 0.25 + AddStep("Move mouse to 0.25x", () => InputManager.MoveMouseTo(new Vector2(scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X, scrollQuad.Centre.Y))); + AddStep("Press alt down", () => InputManager.PressKey(Key.AltLeft)); + AddStep("Zoom by 3", () => InputManager.ScrollBy(new Vector2(0, 3))); + AddStep("Release alt", () => InputManager.ReleaseKey(Key.AltLeft)); + + AddStep("Scroll far left", () => InputManager.ScrollBy(new Vector2(0, 30))); + AddUntilStep("Scroll is at start", () => Precision.AlmostEquals(scrollQuad.TopLeft.X, boxQuad.TopLeft.X, 1)); + + AddStep("Scroll far right", () => InputManager.ScrollBy(new Vector2(0, -300))); + AddUntilStep("Scroll is at end", () => Precision.AlmostEquals(scrollQuad.TopRight.X, boxQuad.TopRight.X, 1)); } [Test] @@ -103,6 +124,8 @@ namespace osu.Game.Tests.Visual.Editing { reset(); + AddStep("Press alt down", () => InputManager.PressKey(Key.AltLeft)); + // Scroll in at 0.25 AddStep("Move mouse to 0.25x", () => InputManager.MoveMouseTo(new Vector2(scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X, scrollQuad.Centre.Y))); AddStep("Scroll by 1", () => InputManager.ScrollBy(new Vector2(0, 1))); @@ -124,6 +147,8 @@ namespace osu.Game.Tests.Visual.Editing AddStep("Move mouse to 0.25x", () => InputManager.MoveMouseTo(new Vector2(scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X, scrollQuad.Centre.Y))); AddStep("Scroll by -1", () => InputManager.ScrollBy(new Vector2(0, -1))); AddAssert("Box at 0", () => Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft)); + + AddStep("Release alt", () => InputManager.ReleaseKey(Key.AltLeft)); } private void reset() diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index a4df450db9..df4b85b37a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -3,8 +3,10 @@ using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Screens; using osu.Framework.Testing; @@ -227,12 +229,19 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded); } - internal class TestSpectatorStreamingClient : SpectatorStreamingClient + public class TestSpectatorStreamingClient : SpectatorStreamingClient { public readonly User StreamingUser = new User { Id = 1234, Username = "Test user" }; + public new BindableList PlayingUsers => (BindableList)base.PlayingUsers; + private int beatmapId; + protected override Task Connect() + { + return Task.CompletedTask; + } + public void StartPlay(int beatmapId) { this.beatmapId = beatmapId; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 1d8231cce7..35473ee76c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Gameplay private Replay replay; - private IBindableList users; + private readonly IBindableList users = new BindableList(); private TestReplayRecorder recorder; @@ -67,7 +67,7 @@ namespace osu.Game.Tests.Visual.Gameplay { replay = new Replay(); - users = streamingClient.PlayingUsers.GetBoundCopy(); + users.BindTo(streamingClient.PlayingUsers); users.BindCollectionChanged((obj, args) => { switch (args.Action) diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs new file mode 100644 index 0000000000..d6fd33bce7 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs @@ -0,0 +1,66 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.Spectator; +using osu.Game.Overlays.Dashboard; +using osu.Game.Tests.Visual.Gameplay; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneCurrentlyPlayingDisplay : OsuTestScene + { + [Cached(typeof(SpectatorStreamingClient))] + private TestSceneSpectator.TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSceneSpectator.TestSpectatorStreamingClient(); + + private CurrentlyPlayingDisplay currentlyPlaying; + + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("register request handling", () => dummyAPI.HandleRequest = req => + { + switch (req) + { + case GetUserRequest cRequest: + cRequest.TriggerSuccess(new User { Username = "peppy", Id = 2 }); + break; + } + }); + + AddStep("add streaming client", () => + { + Remove(testSpectatorStreamingClient); + + Children = new Drawable[] + { + testSpectatorStreamingClient, + currentlyPlaying = new CurrentlyPlayingDisplay + { + RelativeSizeAxes = Axes.Both, + } + }; + }); + + AddStep("Reset players", () => testSpectatorStreamingClient.PlayingUsers.Clear()); + } + + [Test] + public void TestBasicDisplay() + { + AddStep("Add playing user", () => testSpectatorStreamingClient.PlayingUsers.Add(2)); + AddUntilStep("Panel loaded", () => currentlyPlaying.ChildrenOfType()?.FirstOrDefault()?.User.Id == 2); + AddStep("Remove playing user", () => testSpectatorStreamingClient.PlayingUsers.Remove(2)); + AddUntilStep("Panel no longer present", () => !currentlyPlaying.ChildrenOfType().Any()); + } + } +} diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index e0971d238a..8206a92a54 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -42,6 +42,8 @@ namespace osu.Game.Configuration Set(OsuSetting.Username, string.Empty); Set(OsuSetting.Token, string.Empty); + Set(OsuSetting.AutomaticallyDownloadWhenSpectating, false); + Set(OsuSetting.SavePassword, false).ValueChanged += enabled => { if (enabled.NewValue) Set(OsuSetting.SaveUsername, true); @@ -132,6 +134,8 @@ namespace osu.Game.Configuration Set(OsuSetting.MenuBackgroundSource, BackgroundSource.Skin); Set(OsuSetting.SeasonalBackgroundMode, SeasonalBackgroundMode.Sometimes); + + Set(OsuSetting.EditorWaveformOpacity, 1f); } public OsuConfigManager(Storage storage) @@ -241,6 +245,8 @@ namespace osu.Game.Configuration HitLighting, MenuBackgroundSource, GameplayDisableWinKey, - SeasonalBackgroundMode + SeasonalBackgroundMode, + EditorWaveformOpacity, + AutomaticallyDownloadWhenSpectating, } } diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 2367651e04..9ba81720d8 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -93,14 +93,14 @@ namespace osu.Game.Online.Spectator break; case APIState.Online: - Task.Run(connect); + Task.Run(Connect); break; } } private const string endpoint = "https://spectator.ppy.sh/spectator"; - private async Task connect() + protected virtual async Task Connect() { if (connection != null) return; diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs new file mode 100644 index 0000000000..697ceacf0a --- /dev/null +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -0,0 +1,132 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Specialized; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Screens; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.Spectator; +using osu.Game.Screens.Multi.Match.Components; +using osu.Game.Screens.Play; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Overlays.Dashboard +{ + internal class CurrentlyPlayingDisplay : CompositeDrawable + { + private readonly IBindableList playingUsers = new BindableList(); + + private FillFlowContainer userFlow; + + [Resolved] + private SpectatorStreamingClient spectatorStreaming { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChild = userFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(10), + }; + } + + [Resolved] + private IAPIProvider api { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + playingUsers.BindTo(spectatorStreaming.PlayingUsers); + playingUsers.BindCollectionChanged((sender, e) => Schedule(() => + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + foreach (var u in e.NewItems.OfType()) + { + var request = new GetUserRequest(u); + request.Success += user => Schedule(() => + { + if (playingUsers.Contains((int)user.Id)) + userFlow.Add(createUserPanel(user)); + }); + api.Queue(request); + } + + break; + + case NotifyCollectionChangedAction.Remove: + foreach (var u in e.OldItems.OfType()) + userFlow.FirstOrDefault(card => card.User.Id == u)?.Expire(); + break; + + case NotifyCollectionChangedAction.Reset: + userFlow.Clear(); + break; + } + }), true); + } + + private PlayingUserPanel createUserPanel(User user) => + new PlayingUserPanel(user).With(panel => + { + panel.Anchor = Anchor.TopCentre; + panel.Origin = Anchor.TopCentre; + }); + + private class PlayingUserPanel : CompositeDrawable + { + public readonly User User; + + [Resolved(canBeNull: true)] + private OsuGame game { get; set; } + + public PlayingUserPanel(User user) + { + User = user; + + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(2), + Width = 290, + Children = new Drawable[] + { + new UserGridPanel(user) + { + RelativeSizeAxes = Axes.X, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }, + new PurpleTriangleButton + { + RelativeSizeAxes = Axes.X, + Text = "Watch", + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Action = () => game?.PerformFromScreen(s => s.Push(new Spectator(user))) + } + } + }, + }; + } + } + } +} diff --git a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs index 36bf589877..3314ed957a 100644 --- a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs +++ b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.ComponentModel; + namespace osu.Game.Overlays.Dashboard { public class DashboardOverlayHeader : TabControlOverlayHeader @@ -20,6 +22,9 @@ namespace osu.Game.Overlays.Dashboard public enum DashboardOverlayTabs { - Friends + Friends, + + [Description("Currently Playing")] + CurrentlyPlaying } } diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index a2490365e4..787a4985d7 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.cs @@ -130,6 +130,11 @@ namespace osu.Game.Overlays loadDisplay(new FriendDisplay()); break; + case DashboardOverlayTabs.CurrentlyPlaying: + //todo: enable once caching logic is better + //loadDisplay(new CurrentlyPlayingDisplay()); + break; + default: throw new NotImplementedException($"Display for {tab.NewValue} tab is not implemented"); } diff --git a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs index 6461bd7b93..8134c350a6 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs @@ -27,6 +27,12 @@ namespace osu.Game.Overlays.Settings.Sections.Online Keywords = new[] { "no-video" }, Current = config.GetBindable(OsuSetting.PreferNoVideo) }, + new SettingsCheckbox + { + LabelText = "Automatically download beatmaps when spectating", + Keywords = new[] { "spectator" }, + Current = config.GetBindable(OsuSetting.AutomaticallyDownloadWhenSpectating), + }, }; } } diff --git a/osu.Game/Overlays/TabControlOverlayHeader.cs b/osu.Game/Overlays/TabControlOverlayHeader.cs index 61605d9e9e..7798dfa576 100644 --- a/osu.Game/Overlays/TabControlOverlayHeader.cs +++ b/osu.Game/Overlays/TabControlOverlayHeader.cs @@ -1,9 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -104,7 +106,7 @@ namespace osu.Game.Overlays public OverlayHeaderTabItem(T value) : base(value) { - Text.Text = value.ToString().ToLower(); + Text.Text = ((Value as Enum)?.GetDescription() ?? Value.ToString()).ToLower(); Text.Font = OsuFont.GetFont(size: 14); Text.Margin = new MarginPadding { Vertical = 16.5f }; // 15px padding + 1.5px line-height difference compensation Bar.Margin = new MarginPadding { Bottom = bar_height }; diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs index afd9e3d760..c6787a1fb1 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs @@ -163,30 +163,27 @@ namespace osu.Game.Screens.Edit.Components.Menus protected override Framework.Graphics.UserInterface.Menu CreateSubMenu() => new SubMenu(); - protected override DrawableMenuItem CreateDrawableMenuItem(MenuItem item) => new DrawableSubMenuItem(item); - - private class DrawableSubMenuItem : DrawableOsuMenuItem + protected override DrawableMenuItem CreateDrawableMenuItem(MenuItem item) { - public DrawableSubMenuItem(MenuItem item) + switch (item) + { + case EditorMenuItemSpacer spacer: + return new DrawableSpacer(spacer); + } + + return base.CreateDrawableMenuItem(item); + } + + private class DrawableSpacer : DrawableOsuMenuItem + { + public DrawableSpacer(MenuItem item) : base(item) { } - protected override bool OnHover(HoverEvent e) - { - if (Item is EditorMenuItemSpacer) - return true; + protected override bool OnHover(HoverEvent e) => true; - return base.OnHover(e); - } - - protected override bool OnClick(ClickEvent e) - { - if (Item is EditorMenuItemSpacer) - return true; - - return base.OnClick(e); - } + protected override bool OnClick(ClickEvent e) => true; } } } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs index 8c0e31c04c..ba3ac9113e 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs @@ -14,13 +14,13 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts /// public class ControlPointPart : TimelinePart { - private IBindableList controlPointGroups; + private readonly IBindableList controlPointGroups = new BindableList(); protected override void LoadBeatmap(WorkingBeatmap beatmap) { base.LoadBeatmap(beatmap); - controlPointGroups = beatmap.Beatmap.ControlPointInfo.Groups.GetBoundCopy(); + controlPointGroups.BindTo(beatmap.Beatmap.ControlPointInfo.Groups); controlPointGroups.BindCollectionChanged((sender, args) => { switch (args.Action) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs index b9eb53b697..93fe6f9989 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs @@ -15,7 +15,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { public readonly ControlPointGroup Group; - private BindableList controlPoints; + private readonly IBindableList controlPoints = new BindableList(); [Resolved] private OsuColour colours { get; set; } @@ -30,7 +30,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { base.LoadComplete(); - controlPoints = (BindableList)Group.ControlPoints.GetBoundCopy(); + controlPoints.BindTo(Group.ControlPoints); controlPoints.BindCollectionChanged((_, __) => { if (controlPoints.Count == 0) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index b753c45cca..742d433760 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -7,17 +7,19 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; using osu.Game.Graphics; using osuTK; +using osuTK.Input; namespace osu.Game.Screens.Edit.Compose.Components { public class SelectionBox : CompositeDrawable { - public Action OnRotation; - public Action OnScale; - public Action OnFlip; - public Action OnReverse; + public Func OnRotation; + public Func OnScale; + public Func OnFlip; + public Func OnReverse; public Action OperationStarted; public Action OperationEnded; @@ -105,6 +107,26 @@ namespace osu.Game.Screens.Edit.Compose.Components recreate(); } + protected override bool OnKeyDown(KeyDownEvent e) + { + if (e.Repeat || !e.ControlPressed) + return false; + + switch (e.Key) + { + case Key.G: + return CanReverse && OnReverse?.Invoke() == true; + + case Key.H: + return CanScaleX && OnFlip?.Invoke(Direction.Horizontal) == true; + + case Key.J: + return CanScaleY && OnFlip?.Invoke(Direction.Vertical) == true; + } + + return base.OnKeyDown(e); + } + private void recreate() { if (LoadState < LoadState.Loading) @@ -143,7 +165,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (CanScaleX && CanScaleY) addFullScaleComponents(); if (CanScaleY) addYScaleComponents(); if (CanRotate) addRotationComponents(); - if (CanReverse) addButton(FontAwesome.Solid.Backward, "Reverse pattern", () => OnReverse?.Invoke()); + if (CanReverse) addButton(FontAwesome.Solid.Backward, "Reverse pattern (Ctrl-G)", () => OnReverse?.Invoke()); } private void addRotationComponents() @@ -178,7 +200,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void addYScaleComponents() { - addButton(FontAwesome.Solid.ArrowsAltV, "Flip vertically", () => OnFlip?.Invoke(Direction.Vertical)); + addButton(FontAwesome.Solid.ArrowsAltV, "Flip vertically (Ctrl-J)", () => OnFlip?.Invoke(Direction.Vertical)); addDragHandle(Anchor.TopCentre); addDragHandle(Anchor.BottomCentre); @@ -194,7 +216,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void addXScaleComponents() { - addButton(FontAwesome.Solid.ArrowsAltH, "Flip horizontally", () => OnFlip?.Invoke(Direction.Horizontal)); + addButton(FontAwesome.Solid.ArrowsAltH, "Flip horizontally (Ctrl-H)", () => OnFlip?.Invoke(Direction.Horizontal)); addDragHandle(Anchor.CentreLeft); addDragHandle(Anchor.CentreRight); diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index d5c83576e2..0bbbfaf5e8 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -99,10 +99,10 @@ namespace osu.Game.Screens.Edit.Compose.Components OperationStarted = OnOperationBegan, OperationEnded = OnOperationEnded, - OnRotation = angle => HandleRotation(angle), - OnScale = (amount, anchor) => HandleScale(amount, anchor), - OnFlip = direction => HandleFlip(direction), - OnReverse = () => HandleReverse(), + OnRotation = HandleRotation, + OnScale = HandleScale, + OnFlip = HandleFlip, + OnReverse = HandleReverse, }; /// diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 9aff4ddf8f..7233faa955 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osuTK; @@ -67,8 +68,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private TimelineControlPointDisplay controlPoints; + private Bindable waveformOpacity; + [BackgroundDependencyLoader] - private void load(IBindable beatmap, OsuColour colours) + private void load(IBindable beatmap, OsuColour colours, OsuConfigManager config) { AddRange(new Drawable[] { @@ -95,7 +98,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline // We don't want the centre marker to scroll AddInternal(new CentreMarker { Depth = float.MaxValue }); - WaveformVisible.ValueChanged += visible => waveform.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint); + waveformOpacity = config.GetBindable(OsuSetting.EditorWaveformOpacity); + waveformOpacity.BindValueChanged(_ => updateWaveformOpacity(), true); + + WaveformVisible.ValueChanged += _ => updateWaveformOpacity(); ControlPointsVisible.ValueChanged += visible => controlPoints.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint); TicksVisible.ValueChanged += visible => ticks.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint); @@ -115,6 +121,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }, true); } + private void updateWaveformOpacity() => + waveform.FadeTo(WaveformVisible.Value ? waveformOpacity.Value : 0, 200, Easing.OutQuint); + private float getZoomLevelForVisibleMilliseconds(double milliseconds) => Math.Max(1, (float)(track.Length / milliseconds)); protected override void Update() @@ -165,6 +174,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (!track.IsLoaded || track.Length == 0) return; + // covers the case where the user starts playback after a drag is in progress. + // we want to ensure the clock is always stopped during drags to avoid weird audio playback. + if (handlingDragInput) + editorClock.Stop(); + ScrollTo((float)(editorClock.CurrentTime / track.Length) * Content.DrawWidth, false); } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs index 3f13e8e5d4..0da1b43201 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// public class TimelineControlPointDisplay : TimelinePart { - private IBindableList controlPointGroups; + private readonly IBindableList controlPointGroups = new BindableList(); public TimelineControlPointDisplay() { @@ -27,7 +27,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { base.LoadBeatmap(beatmap); - controlPointGroups = beatmap.Beatmap.ControlPointInfo.Groups.GetBoundCopy(); + controlPointGroups.BindTo(beatmap.Beatmap.ControlPointInfo.Groups); controlPointGroups.BindCollectionChanged((sender, args) => { switch (args.Action) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs index e32616a574..fb69f16792 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs @@ -14,7 +14,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public readonly ControlPointGroup Group; - private BindableList controlPoints; + private readonly IBindableList controlPoints = new BindableList(); [Resolved] private OsuColour colours { get; set; } @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { base.LoadComplete(); - controlPoints = (BindableList)Group.ControlPoints.GetBoundCopy(); + controlPoints.BindTo(Group.ControlPoints); controlPoints.BindCollectionChanged((_, __) => { ClearInternal(); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs index f90658e99c..f10eb0d284 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs @@ -113,19 +113,19 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override bool OnScroll(ScrollEvent e) { - if (e.IsPrecise) + if (e.AltPressed) { - // can't handle scroll correctly while playing. - // the editor will handle this case for us. - if (editorClock?.IsRunning == true) - return false; - - // for now, we don't support zoom when using a precision scroll device. this needs gesture support. - return base.OnScroll(e); + // zoom when holding alt. + setZoomTarget(zoomTarget + e.ScrollDelta.Y, zoomedContent.ToLocalSpace(e.ScreenSpaceMousePosition).X); + return true; } - setZoomTarget(zoomTarget + e.ScrollDelta.Y, zoomedContent.ToLocalSpace(e.ScreenSpaceMousePosition).X); - return true; + // can't handle scroll correctly while playing. + // the editor will handle this case for us. + if (editorClock?.IsRunning == true) + return false; + + return base.OnScroll(e); } private void updateZoomedContentWidth() => zoomedContent.Width = DrawWidth * currentZoom; diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index f95c7fe7a6..13d1f378a6 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -20,6 +20,7 @@ using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Timing; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; @@ -103,7 +104,7 @@ namespace osu.Game.Screens.Edit private MusicController music { get; set; } [BackgroundDependencyLoader] - private void load(OsuColour colours, GameHost host) + private void load(OsuColour colours, GameHost host, OsuConfigManager config) { beatDivisor.Value = Beatmap.Value.BeatmapInfo.BeatDivisor; beatDivisor.BindValueChanged(divisor => Beatmap.Value.BeatmapInfo.BeatDivisor = divisor.NewValue); @@ -208,6 +209,13 @@ namespace osu.Game.Screens.Edit copyMenuItem = new EditorMenuItem("Copy", MenuItemType.Standard, Copy), pasteMenuItem = new EditorMenuItem("Paste", MenuItemType.Standard, Paste), } + }, + new MenuItem("View") + { + Items = new[] + { + new WaveformOpacityMenu(config) + } } } } diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 64f9526816..89d3c36250 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -98,7 +98,7 @@ namespace osu.Game.Screens.Edit.Timing private class ControlGroupAttributes : CompositeDrawable { - private readonly IBindableList controlPoints; + private readonly IBindableList controlPoints = new BindableList(); private readonly FillFlowContainer fill; @@ -112,7 +112,7 @@ namespace osu.Game.Screens.Edit.Timing Spacing = new Vector2(2) }; - controlPoints = group.ControlPoints.GetBoundCopy(); + controlPoints.BindTo(group.ControlPoints); } [Resolved] diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index f511382cde..09d861522a 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -56,7 +56,7 @@ namespace osu.Game.Screens.Edit.Timing private OsuButton deleteButton; private ControlPointTable table; - private IBindableList controlGroups; + private readonly IBindableList controlPointGroups = new BindableList(); [Resolved] private EditorClock clock { get; set; } @@ -124,11 +124,10 @@ namespace osu.Game.Screens.Edit.Timing selectedGroup.BindValueChanged(selected => { deleteButton.Enabled.Value = selected.NewValue != null; }, true); - controlGroups = Beatmap.Value.Beatmap.ControlPointInfo.Groups.GetBoundCopy(); - - controlGroups.BindCollectionChanged((sender, args) => + controlPointGroups.BindTo(Beatmap.Value.Beatmap.ControlPointInfo.Groups); + controlPointGroups.BindCollectionChanged((sender, args) => { - table.ControlGroups = controlGroups; + table.ControlGroups = controlPointGroups; changeHandler.SaveState(); }, true); } diff --git a/osu.Game/Screens/Edit/WaveformOpacityMenu.cs b/osu.Game/Screens/Edit/WaveformOpacityMenu.cs new file mode 100644 index 0000000000..5d209ae141 --- /dev/null +++ b/osu.Game/Screens/Edit/WaveformOpacityMenu.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Bindables; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Configuration; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Screens.Edit +{ + internal class WaveformOpacityMenu : MenuItem + { + private readonly Bindable waveformOpacity; + + private readonly Dictionary menuItemLookup = new Dictionary(); + + public WaveformOpacityMenu(OsuConfigManager config) + : base("Waveform opacity") + { + Items = new[] + { + createMenuItem(0.25f), + createMenuItem(0.5f), + createMenuItem(0.75f), + createMenuItem(1f), + }; + + waveformOpacity = config.GetBindable(OsuSetting.EditorWaveformOpacity); + waveformOpacity.BindValueChanged(opacity => + { + foreach (var kvp in menuItemLookup) + kvp.Value.State.Value = kvp.Key == opacity.NewValue; + }, true); + } + + private ToggleMenuItem createMenuItem(float opacity) + { + var item = new ToggleMenuItem($"{opacity * 100}%", MenuItemType.Standard, _ => updateOpacity(opacity)); + menuItemLookup[opacity] = item; + return item; + } + + private void updateOpacity(float opacity) => waveformOpacity.Value = opacity; + } +} diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs index 2f65dc06d0..9ed911efd5 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/Spectator.cs @@ -9,20 +9,26 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Spectator; using osu.Game.Overlays.BeatmapListing.Panels; +using osu.Game.Overlays.Settings; using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using osu.Game.Scoring; +using osu.Game.Screens.Multi.Match.Components; using osu.Game.Users; using osuTK; @@ -63,6 +69,12 @@ namespace osu.Game.Screens.Play private IBindable> managerUpdated; + private TriangleButton watchButton; + + private SettingsCheckbox automaticDownload; + + private BeatmapSetInfo onlineBeatmap; + /// /// Becomes true if a new state is waiting to be loaded (while this screen was not active). /// @@ -74,47 +86,91 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader] - private void load() + private void load(OsuColour colours, OsuConfigManager config) { - InternalChildren = new Drawable[] + InternalChild = new Container { - new FillFlowContainer + Masking = true, + CornerRadius = 20, + AutoSizeAxes = Axes.Both, + AutoSizeDuration = 500, + AutoSizeEasing = Easing.OutQuint, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Spacing = new Vector2(15), - Children = new Drawable[] + new Box { - new OsuSpriteText + Colour = colours.GreySeafoamDark, + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + Margin = new MarginPadding(20), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(15), + Children = new Drawable[] { - Text = "Currently spectating", - Font = OsuFont.Default.With(size: 30), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new UserGridPanel(targetUser) - { - Width = 290, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new OsuSpriteText - { - Text = "playing", - Font = OsuFont.Default.With(size: 30), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - beatmapPanelContainer = new Container - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, + new OsuSpriteText + { + Text = "Spectator Mode", + Font = OsuFont.Default.With(size: 30), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(15), + Children = new Drawable[] + { + new UserGridPanel(targetUser) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Height = 145, + Width = 290, + }, + new SpriteIcon + { + Size = new Vector2(40), + Icon = FontAwesome.Solid.ArrowRight, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + beatmapPanelContainer = new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + } + }, + automaticDownload = new SettingsCheckbox + { + LabelText = "Automatically download beatmaps", + Current = config.GetBindable(OsuSetting.AutomaticallyDownloadWhenSpectating), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + watchButton = new PurpleTriangleButton + { + Text = "Start Watching", + Width = 250, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Action = attemptStart, + Enabled = { Value = false } + } + } } - }, + } }; } @@ -130,12 +186,14 @@ namespace osu.Game.Screens.Play managerUpdated = beatmaps.ItemUpdated.GetBoundCopy(); managerUpdated.BindValueChanged(beatmapUpdated); + + automaticDownload.Current.BindValueChanged(_ => checkForAutomaticDownload()); } private void beatmapUpdated(ValueChangedEvent> beatmap) { if (beatmap.NewValue.TryGetTarget(out var beatmapSet) && beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID == state.BeatmapID)) - attemptStart(); + Schedule(attemptStart); } private void userSentFrames(int userId, FrameDataBundle data) @@ -189,14 +247,26 @@ namespace osu.Game.Screens.Play if (userId != targetUser.Id) return; - if (replay == null) return; + if (replay != null) + { + replay.HasReceivedAllFrames = true; + replay = null; + } - replay.HasReceivedAllFrames = true; - replay = null; + Schedule(clearDisplay); + } + + private void clearDisplay() + { + watchButton.Enabled.Value = false; + beatmapPanelContainer.Clear(); } private void attemptStart() { + clearDisplay(); + showBeatmapPanel(state); + var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.ID == state.RulesetID)?.CreateInstance(); // ruleset not available @@ -210,7 +280,6 @@ namespace osu.Game.Screens.Play if (resolvedBeatmap == null) { - showBeatmapPanel(state.BeatmapID.Value); return; } @@ -228,6 +297,7 @@ namespace osu.Game.Screens.Play rulesetInstance = resolvedRuleset; beatmap.Value = beatmaps.GetWorkingBeatmap(resolvedBeatmap); + watchButton.Enabled.Value = true; this.Push(new SpectatorPlayerLoader(new Score { @@ -236,17 +306,43 @@ namespace osu.Game.Screens.Play })); } - private void showBeatmapPanel(int beatmapId) + private void showBeatmapPanel(SpectatorState state) { - var req = new GetBeatmapSetRequest(beatmapId, BeatmapSetLookupType.BeatmapId); + if (state?.BeatmapID == null) + { + beatmapPanelContainer.Clear(); + onlineBeatmap = null; + return; + } + + var req = new GetBeatmapSetRequest(state.BeatmapID.Value, BeatmapSetLookupType.BeatmapId); req.Success += res => Schedule(() => { - beatmapPanelContainer.Child = new GridBeatmapPanel(res.ToBeatmapSet(rulesets)); + if (state != this.state) + return; + + onlineBeatmap = res.ToBeatmapSet(rulesets); + beatmapPanelContainer.Child = new GridBeatmapPanel(onlineBeatmap); + checkForAutomaticDownload(); }); api.Queue(req); } + private void checkForAutomaticDownload() + { + if (onlineBeatmap == null) + return; + + if (!automaticDownload.Current.Value) + return; + + if (beatmaps.IsAvailableLocally(onlineBeatmap)) + return; + + beatmaps.Download(onlineBeatmap); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index 6c1e83f236..fdf996150f 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -3,33 +3,60 @@ using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; using osu.Game.Online.Spectator; using osu.Game.Scoring; +using osu.Game.Screens.Ranking; namespace osu.Game.Screens.Play { - public class SpectatorPlayer : ReplayPlayer + public class SpectatorPlayer : Player { - [Resolved] - private SpectatorStreamingClient spectatorStreaming { get; set; } + private readonly Score score; + + protected override bool CheckModsAllowFailure() => false; // todo: better support starting mid-way through beatmap public SpectatorPlayer(Score score) - : base(score) { + this.score = score; } + protected override ResultsScreen CreateResults(ScoreInfo score) + { + return new SpectatorResultsScreen(score); + } + + [Resolved] + private SpectatorStreamingClient spectatorStreaming { get; set; } + [BackgroundDependencyLoader] private void load() { spectatorStreaming.OnUserBeganPlaying += userBeganPlaying; + + AddInternal(new OsuSpriteText + { + Text = $"Watching {score.ScoreInfo.User.Username} playing live!", + Font = OsuFont.Default.With(size: 30), + Y = 100, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }); + } + + protected override void PrepareReplay() + { + DrawableRuleset?.SetReplayScore(score); } protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) { // if we already have frames, start gameplay at the point in time they exist, should they be too far into the beatmap. - double? firstFrameTime = Score.Replay.Frames.FirstOrDefault()?.Time; + double? firstFrameTime = score.Replay.Frames.FirstOrDefault()?.Time; if (firstFrameTime == null || firstFrameTime <= gameplayStart + 5000) return base.CreateGameplayClockContainer(beatmap, gameplayStart); @@ -43,6 +70,16 @@ namespace osu.Game.Screens.Play return base.OnExiting(next); } + private void userBeganPlaying(int userId, SpectatorState state) + { + if (userId != score.ScoreInfo.UserID) return; + + Schedule(() => + { + if (this.IsCurrentScreen()) this.Exit(); + }); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -50,11 +87,5 @@ namespace osu.Game.Screens.Play if (spectatorStreaming != null) spectatorStreaming.OnUserBeganPlaying -= userBeganPlaying; } - - private void userBeganPlaying(int userId, SpectatorState state) - { - if (userId == Score.ScoreInfo.UserID) - Schedule(this.Exit); - } } } diff --git a/osu.Game/Screens/Play/SpectatorResultsScreen.cs b/osu.Game/Screens/Play/SpectatorResultsScreen.cs new file mode 100644 index 0000000000..56ccfd2253 --- /dev/null +++ b/osu.Game/Screens/Play/SpectatorResultsScreen.cs @@ -0,0 +1,47 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Screens; +using osu.Game.Online.Spectator; +using osu.Game.Scoring; +using osu.Game.Screens.Ranking; + +namespace osu.Game.Screens.Play +{ + public class SpectatorResultsScreen : SoloResultsScreen + { + public SpectatorResultsScreen(ScoreInfo score) + : base(score) + { + } + + [Resolved] + private SpectatorStreamingClient spectatorStreaming { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + spectatorStreaming.OnUserBeganPlaying += userBeganPlaying; + } + + private void userBeganPlaying(int userId, SpectatorState state) + { + if (userId == Score.UserID) + { + Schedule(() => + { + if (this.IsCurrentScreen()) this.Exit(); + }); + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (spectatorStreaming != null) + spectatorStreaming.OnUserBeganPlaying -= userBeganPlaying; + } + } +} diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index bdfcc2fd96..2634f117de 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using JetBrains.Annotations; using osuTK; using osuTK.Graphics; @@ -85,6 +86,8 @@ namespace osu.Game.Screens.Select private WorkingBeatmap beatmap; + private CancellationTokenSource cancellationSource; + public WorkingBeatmap Beatmap { get => beatmap; @@ -93,9 +96,11 @@ namespace osu.Game.Screens.Select if (beatmap == value) return; beatmap = value; + cancellationSource?.Cancel(); + cancellationSource = new CancellationTokenSource(); beatmapDifficulty?.UnbindAll(); - beatmapDifficulty = difficultyManager.GetBindableDifficulty(beatmap.BeatmapInfo); + beatmapDifficulty = difficultyManager.GetBindableDifficulty(beatmap.BeatmapInfo, cancellationSource.Token); beatmapDifficulty.BindValueChanged(_ => updateDisplay()); updateDisplay(); @@ -108,33 +113,44 @@ namespace osu.Game.Screens.Select private void updateDisplay() { - void removeOldInfo() - { - State.Value = beatmap == null ? Visibility.Hidden : Visibility.Visible; + Scheduler.AddOnce(perform); - Info?.FadeOut(250); - Info?.Expire(); - Info = null; + void perform() + { + void removeOldInfo() + { + State.Value = beatmap == null ? Visibility.Hidden : Visibility.Visible; + + Info?.FadeOut(250); + Info?.Expire(); + Info = null; + } + + if (beatmap == null) + { + removeOldInfo(); + return; + } + + LoadComponentAsync(loadingInfo = new BufferedWedgeInfo(beatmap, ruleset.Value, beatmapDifficulty.Value) + { + Shear = -Shear, + Depth = Info?.Depth + 1 ?? 0 + }, loaded => + { + // ensure we are the most recent loaded wedge. + if (loaded != loadingInfo) return; + + removeOldInfo(); + Add(Info = loaded); + }); } + } - if (beatmap == null) - { - removeOldInfo(); - return; - } - - LoadComponentAsync(loadingInfo = new BufferedWedgeInfo(beatmap, ruleset.Value, beatmapDifficulty.Value) - { - Shear = -Shear, - Depth = Info?.Depth + 1 ?? 0 - }, loaded => - { - // ensure we are the most recent loaded wedge. - if (loaded != loadingInfo) return; - - removeOldInfo(); - Add(Info = loaded); - }); + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + cancellationSource?.Cancel(); } public class BufferedWedgeInfo : BufferedContainer diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index def620462f..b55c0694ef 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -37,6 +37,7 @@ using osu.Framework.Input.Bindings; using osu.Game.Collections; using osu.Game.Graphics.UserInterface; using osu.Game.Scoring; +using System.Diagnostics; namespace osu.Game.Screens.Select { @@ -519,7 +520,7 @@ namespace osu.Game.Screens.Select ModSelect.SelectedMods.BindTo(selectedMods); - music.TrackChanged += ensureTrackLooping; + beginLooping(); } private const double logo_transition = 250; @@ -570,8 +571,7 @@ namespace osu.Game.Screens.Select BeatmapDetails.Refresh(); - music.CurrentTrack.Looping = true; - music.TrackChanged += ensureTrackLooping; + beginLooping(); music.ResetTrackAdjustments(); if (Beatmap != null && !Beatmap.Value.BeatmapSetInfo.DeletePending) @@ -597,8 +597,7 @@ namespace osu.Game.Screens.Select BeatmapOptions.Hide(); - music.CurrentTrack.Looping = false; - music.TrackChanged -= ensureTrackLooping; + endLooping(); this.ScaleTo(1.1f, 250, Easing.InSine); @@ -619,12 +618,33 @@ namespace osu.Game.Screens.Select FilterControl.Deactivate(); - music.CurrentTrack.Looping = false; - music.TrackChanged -= ensureTrackLooping; + endLooping(); return false; } + private bool isHandlingLooping; + + private void beginLooping() + { + Debug.Assert(!isHandlingLooping); + + music.CurrentTrack.Looping = isHandlingLooping = true; + + music.TrackChanged += ensureTrackLooping; + } + + private void endLooping() + { + // may be called multiple times during screen exit process. + if (!isHandlingLooping) + return; + + music.CurrentTrack.Looping = isHandlingLooping = false; + + music.TrackChanged -= ensureTrackLooping; + } + private void ensureTrackLooping(WorkingBeatmap beatmap, TrackChangeDirection changeDirection) => music.CurrentTrack.Looping = true; diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 57a87a713d..0981136dba 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -20,6 +20,10 @@ namespace osu.Game.Users { public readonly User User; + /// + /// Perform an action in addition to showing the user's profile. + /// This should be used to perform auxiliary tasks and not as a primary action for clicking a user panel (to maintain a consistent UX). + /// public new Action Action; protected Action ViewProfile { get; private set; } diff --git a/osu.iOS.props b/osu.iOS.props index 76c496cd2d..40ecfffcca 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -73,6 +73,14 @@ + + + $(NoWarn);NU1605 + + + + +