From d9ba677773fc3945c3e62d6f16a9bdf5ec2f9fa5 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 24 Aug 2020 15:08:50 +0200 Subject: [PATCH 01/47] Change TeamFlag from sprite to a container with a sprite --- .../Components/DrawableTeamFlag.cs | 20 +++++++++++++++++-- .../Components/DrawableTournamentTeam.cs | 11 ++-------- .../Ladder/Components/DrawableMatchTeam.cs | 2 +- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tournament/Components/DrawableTeamFlag.cs b/osu.Game.Tournament/Components/DrawableTeamFlag.cs index 8c85c9a46f..a2e0bf83be 100644 --- a/osu.Game.Tournament/Components/DrawableTeamFlag.cs +++ b/osu.Game.Tournament/Components/DrawableTeamFlag.cs @@ -4,19 +4,24 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Tournament.Models; +using osuTK; namespace osu.Game.Tournament.Components { - public class DrawableTeamFlag : Sprite + public class DrawableTeamFlag : Container { private readonly TournamentTeam team; [UsedImplicitly] private Bindable flag; + private Sprite flagSprite; + public DrawableTeamFlag(TournamentTeam team) { this.team = team; @@ -27,7 +32,18 @@ namespace osu.Game.Tournament.Components { if (team == null) return; - (flag = team.FlagName.GetBoundCopy()).BindValueChanged(acronym => Texture = textures.Get($@"Flags/{team.FlagName}"), true); + Size = new Vector2(70, 47); + Masking = true; + CornerRadius = 5; + Child = flagSprite = new Sprite + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fill + }; + + (flag = team.FlagName.GetBoundCopy()).BindValueChanged(acronym => flagSprite.Texture = textures.Get($@"Flags/{team.FlagName}"), true); } } } diff --git a/osu.Game.Tournament/Components/DrawableTournamentTeam.cs b/osu.Game.Tournament/Components/DrawableTournamentTeam.cs index f8aed26ce1..b9442a67f5 100644 --- a/osu.Game.Tournament/Components/DrawableTournamentTeam.cs +++ b/osu.Game.Tournament/Components/DrawableTournamentTeam.cs @@ -4,9 +4,7 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Graphics; using osu.Game.Tournament.Models; @@ -17,7 +15,7 @@ namespace osu.Game.Tournament.Components { public readonly TournamentTeam Team; - protected readonly Sprite Flag; + protected readonly Container Flag; protected readonly TournamentSpriteText AcronymText; [UsedImplicitly] @@ -27,12 +25,7 @@ namespace osu.Game.Tournament.Components { Team = team; - Flag = new DrawableTeamFlag(team) - { - RelativeSizeAxes = Axes.Both, - FillMode = FillMode.Fit - }; - + Flag = new DrawableTeamFlag(team); AcronymText = new TournamentSpriteText { Font = OsuFont.Torus.With(weight: FontWeight.Regular), diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs index 15cb7e44cb..030ccb5cb3 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs @@ -63,7 +63,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components this.losers = losers; Size = new Vector2(150, 40); - Flag.Scale = new Vector2(0.9f); + Flag.Scale = new Vector2(0.6f); Flag.Anchor = Flag.Origin = Anchor.CentreLeft; AcronymText.Anchor = AcronymText.Origin = Anchor.CentreLeft; From 18ae17e1290a936a4f854d6cbf740c2ad9dbe1f0 Mon Sep 17 00:00:00 2001 From: Shivam Date: Sun, 13 Sep 2020 19:55:21 +0200 Subject: [PATCH 02/47] Add scale to GroupTeam and remove unnecessary sizing and scaling in other scenes --- osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs | 1 + osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs | 1 - osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs | 2 -- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs b/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs index 4f0ce0bbe7..119f71ebfa 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs @@ -27,6 +27,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components AcronymText.Origin = Anchor.TopCentre; AcronymText.Text = team.Acronym.Value.ToUpperInvariant(); AcronymText.Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 10); + Flag.Scale = new Vector2(0.5f); InternalChildren = new Drawable[] { diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs index b01c93ae03..48aea46497 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs @@ -29,7 +29,6 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components var anchor = flip ? Anchor.TopLeft : Anchor.TopRight; Flag.RelativeSizeAxes = Axes.None; - Flag.Size = new Vector2(60, 40); Flag.Origin = anchor; Flag.Anchor = anchor; diff --git a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs index 3870f486e1..dde140ab91 100644 --- a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs +++ b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs @@ -90,8 +90,6 @@ namespace osu.Game.Tournament.Screens.TeamWin { new DrawableTeamFlag(match.Winner) { - Size = new Vector2(300, 200), - Scale = new Vector2(0.5f), Anchor = Anchor.Centre, Origin = Anchor.Centre, Position = new Vector2(-300, 10), From afa86f959f184acb76ace6b5b7ae68ff490b8440 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 19 Oct 2020 23:38:06 +0200 Subject: [PATCH 03/47] Changed scales of Seeding and Win screen to match the original These were measured by pixel-to-pixel comparing master vs this branch in ShareX at the same resolution. --- osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs | 3 +-- osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs index b343608e69..32830713f6 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs @@ -288,8 +288,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro AutoSizeAxes = Axes.Both; Flag.RelativeSizeAxes = Axes.None; - Flag.Size = new Vector2(300, 200); - Flag.Scale = new Vector2(0.3f); + Flag.Scale = new Vector2(1.4f); InternalChild = new FillFlowContainer { diff --git a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs index dde140ab91..3972c590ea 100644 --- a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs +++ b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs @@ -93,6 +93,7 @@ namespace osu.Game.Tournament.Screens.TeamWin Anchor = Anchor.Centre, Origin = Anchor.Centre, Position = new Vector2(-300, 10), + Scale = new Vector2(2.2f) }, new FillFlowContainer { From 2c7880e9d6118d01ca5c0150780f1cf136c357df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Oct 2020 18:27:03 +0200 Subject: [PATCH 04/47] Add failing test case --- .../Visual/Gameplay/TestScenePlayerLoader.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 888a2f2197..9b31dd045a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -265,6 +265,26 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddAssert($"epilepsy warning {(warning ? "present" : "absent")}", () => this.ChildrenOfType().Any() == warning); + + if (warning) + { + AddUntilStep("sound volume decreased", () => Beatmap.Value.Track.AggregateVolume.Value == 0.25); + AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1); + } + } + + [Test] + public void TestEpilepsyWarningEarlyExit() + { + AddStep("set epilepsy warning", () => epilepsyWarning = true); + AddStep("load dummy beatmap", () => ResetPlayer(false)); + + AddUntilStep("wait for current", () => loader.IsCurrentScreen()); + + AddUntilStep("wait for epilepsy warning", () => loader.ChildrenOfType().Single().Alpha > 0); + AddStep("exit early", () => loader.Exit()); + + AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1); } private class TestPlayerLoaderContainer : Container From b39a4da6bcec029855c513964a5d29fab3a35977 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 21 Oct 2020 19:05:20 +0900 Subject: [PATCH 05/47] Add initial classes for spectator support --- .../Gameplay/TestSceneReplayRecorder.cs | 20 +++++- osu.Game/Online/Spectator/FrameDataBundle.cs | 17 +++++ osu.Game/Online/Spectator/ISpectatorClient.cs | 14 ++++ osu.Game/Online/Spectator/ISpectatorServer.cs | 14 ++++ osu.Game/Online/Spectator/SpectatorClient.cs | 70 +++++++++++++++++++ osu.Game/Replays/Legacy/LegacyReplayFrame.cs | 2 + osu.Game/osu.Game.csproj | 3 + 7 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Online/Spectator/FrameDataBundle.cs create mode 100644 osu.Game/Online/Spectator/ISpectatorClient.cs create mode 100644 osu.Game/Online/Spectator/ISpectatorServer.cs create mode 100644 osu.Game/Online/Spectator/SpectatorClient.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index bc1c10e59d..88a4024576 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -13,7 +15,9 @@ using osu.Framework.Input.StateChanges; using osu.Framework.Testing; using osu.Framework.Threading; using osu.Game.Graphics.Sprites; +using osu.Game.Online.Spectator; using osu.Game.Replays; +using osu.Game.Replays.Legacy; using osu.Game.Rulesets; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.UI; @@ -260,13 +264,27 @@ namespace osu.Game.Tests.Visual.Gameplay internal class TestReplayRecorder : ReplayRecorder { + private readonly SpectatorClient client; + public TestReplayRecorder(Replay target) : base(target) { + var connection = new HubConnectionBuilder() + .WithUrl("http://localhost:5009/spectator") + .AddMessagePackProtocol() + // .ConfigureLogging(logging => { logging.AddConsole(); }) + .Build(); + + connection.StartAsync().Wait(); + + client = new SpectatorClient(connection); } protected override ReplayFrame HandleFrame(Vector2 mousePosition, List actions, ReplayFrame previousFrame) - => new TestReplayFrame(Time.Current, mousePosition, actions.ToArray()); + { + client.SendFrames(new FrameDataBundle(new[] { new LegacyReplayFrame(Time.Current, mousePosition.X, mousePosition.Y, ReplayButtonState.None) })); + return new TestReplayFrame(Time.Current, mousePosition, actions.ToArray()); + } } } } diff --git a/osu.Game/Online/Spectator/FrameDataBundle.cs b/osu.Game/Online/Spectator/FrameDataBundle.cs new file mode 100644 index 0000000000..67f2688289 --- /dev/null +++ b/osu.Game/Online/Spectator/FrameDataBundle.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using osu.Game.Replays.Legacy; + +namespace osu.Game.Online.Spectator +{ + [Serializable] + public class FrameDataBundle + { + public IEnumerable Frames { get; set; } + + public FrameDataBundle(IEnumerable frames) + { + Frames = frames; + } + } +} diff --git a/osu.Game/Online/Spectator/ISpectatorClient.cs b/osu.Game/Online/Spectator/ISpectatorClient.cs new file mode 100644 index 0000000000..4741d7409a --- /dev/null +++ b/osu.Game/Online/Spectator/ISpectatorClient.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using osu.Game.Online.Spectator; + +namespace osu.Server.Spectator.Hubs +{ + public interface ISpectatorClient + { + Task UserBeganPlaying(string userId, int beatmapId); + + Task UserFinishedPlaying(string userId, int beatmapId); + + Task UserSentFrames(string userId, FrameDataBundle data); + } +} \ No newline at end of file diff --git a/osu.Game/Online/Spectator/ISpectatorServer.cs b/osu.Game/Online/Spectator/ISpectatorServer.cs new file mode 100644 index 0000000000..1dcde30221 --- /dev/null +++ b/osu.Game/Online/Spectator/ISpectatorServer.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; + +namespace osu.Game.Online.Spectator +{ + public interface ISpectatorServer + { + Task BeginPlaySession(int beatmapId); + Task SendFrameData(FrameDataBundle data); + Task EndPlaySession(int beatmapId); + + Task StartWatchingUser(string userId); + Task EndWatchingUser(string userId); + } +} diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs new file mode 100644 index 0000000000..c1414f7914 --- /dev/null +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR.Client; +using osu.Server.Spectator.Hubs; + +namespace osu.Game.Online.Spectator +{ + public class SpectatorClient : ISpectatorClient + { + private readonly HubConnection connection; + + private readonly List watchingUsers = new List(); + + public SpectatorClient(HubConnection connection) + { + this.connection = connection; + + // this is kind of SILLY + // https://github.com/dotnet/aspnetcore/issues/15198 + connection.On(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying); + connection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames); + connection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying); + } + + Task ISpectatorClient.UserBeganPlaying(string userId, int beatmapId) + { + if (connection.ConnectionId != userId) + { + if (watchingUsers.Contains(userId)) + { + Console.WriteLine($"{connection.ConnectionId} received began playing for already watched user {userId}"); + } + else + { + Console.WriteLine($"{connection.ConnectionId} requesting watch other user {userId}"); + WatchUser(userId); + watchingUsers.Add(userId); + } + } + else + { + Console.WriteLine($"{connection.ConnectionId} Received user playing event for self {beatmapId}"); + } + + return Task.CompletedTask; + } + + Task ISpectatorClient.UserFinishedPlaying(string userId, int beatmapId) + { + Console.WriteLine($"{connection.ConnectionId} Received user finished event {beatmapId}"); + return Task.CompletedTask; + } + + Task ISpectatorClient.UserSentFrames(string userId, FrameDataBundle data) + { + Console.WriteLine($"{connection.ConnectionId} Received frames from {userId}: {data.Frames.First().ToString()}"); + return Task.CompletedTask; + } + + public Task BeginPlaying(int beatmapId) => connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), beatmapId); + + public Task SendFrames(FrameDataBundle data) => connection.SendAsync(nameof(ISpectatorServer.SendFrameData), data); + + public Task EndPlaying(int beatmapId) => connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), beatmapId); + + private Task WatchUser(string userId) => connection.SendAsync(nameof(ISpectatorServer.StartWatchingUser), userId); + } +} diff --git a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs index c3cffa8699..656fd1814e 100644 --- a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs +++ b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using MessagePack; using osu.Game.Rulesets.Replays; using osuTK; @@ -8,6 +9,7 @@ namespace osu.Game.Replays.Legacy { public class LegacyReplayFrame : ReplayFrame { + [IgnoreMember] public Vector2 Position => new Vector2(MouseX ?? 0, MouseY ?? 0); public float? MouseX; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index de7bde824f..fd010fcc43 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -21,6 +21,9 @@ + + + From db4dd3182b2494f6aa641855a32928be9465c9d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 13:12:58 +0900 Subject: [PATCH 06/47] Add xmldoc to spectator interfaces --- osu.Game/Online/Spectator/ISpectatorClient.cs | 23 +++++++++++++-- osu.Game/Online/Spectator/ISpectatorServer.cs | 28 +++++++++++++++++++ osu.Game/Online/Spectator/SpectatorClient.cs | 1 - 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Spectator/ISpectatorClient.cs b/osu.Game/Online/Spectator/ISpectatorClient.cs index 4741d7409a..ed762ac1fe 100644 --- a/osu.Game/Online/Spectator/ISpectatorClient.cs +++ b/osu.Game/Online/Spectator/ISpectatorClient.cs @@ -1,14 +1,31 @@ using System.Threading.Tasks; -using osu.Game.Online.Spectator; -namespace osu.Server.Spectator.Hubs +namespace osu.Game.Online.Spectator { + /// + /// An interface defining a spectator client instance. + /// public interface ISpectatorClient { + /// + /// Signals that a user has begun a new play session. + /// + /// The user. + /// The beatmap the user is playing. Task UserBeganPlaying(string userId, int beatmapId); + /// + /// Signals that a user has finished a play session. + /// + /// The user. + /// The beatmap the user has finished playing. Task UserFinishedPlaying(string userId, int beatmapId); + /// + /// Called when new frames are available for a subscribed user's play session. + /// + /// The user. + /// The frame data. Task UserSentFrames(string userId, FrameDataBundle data); } -} \ No newline at end of file +} diff --git a/osu.Game/Online/Spectator/ISpectatorServer.cs b/osu.Game/Online/Spectator/ISpectatorServer.cs index 1dcde30221..03ca37d524 100644 --- a/osu.Game/Online/Spectator/ISpectatorServer.cs +++ b/osu.Game/Online/Spectator/ISpectatorServer.cs @@ -2,13 +2,41 @@ using System.Threading.Tasks; namespace osu.Game.Online.Spectator { + /// + /// An interface defining the spectator server instance. + /// public interface ISpectatorServer { + /// + /// Signal the start of a new play session. + /// + /// The beatmap currently being played. Eventually this should be replaced with more complete metadata. Task BeginPlaySession(int beatmapId); + + /// + /// Send a bundle of frame data for the current play session. + /// + /// The frame data. Task SendFrameData(FrameDataBundle data); + + /// + /// Signal the end of a play session. + /// + /// The beatmap that was completed. This should be replaced with a play token once that flow is established. Task EndPlaySession(int beatmapId); + /// + /// Request spectating data for the specified user. May be called on multiple users and offline users. + /// For offline users, a subscription will be created and data will begin streaming on next play. + /// + /// The user to subscribe to. + /// Task StartWatchingUser(string userId); + + /// + /// Stop requesting spectating data for the specified user. Unsubscribes from receiving further data. + /// + /// The user to unsubscribe from. Task EndWatchingUser(string userId); } } diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index c1414f7914..4558699618 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; -using osu.Server.Spectator.Hubs; namespace osu.Game.Online.Spectator { From 93db75bd414e90bfd9eb655f3b022bdface0f3cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 13:41:54 +0900 Subject: [PATCH 07/47] Begin shaping the spectator streaming component --- .../Gameplay/TestSceneReplayRecorder.cs | 14 ----- ...rClient.cs => SpectatorStreamingClient.cs} | 60 ++++++++++++++++--- osu.Game/OsuGameBase.cs | 7 ++- 3 files changed, 58 insertions(+), 23 deletions(-) rename osu.Game/Online/Spectator/{SpectatorClient.cs => SpectatorStreamingClient.cs} (58%) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index 88a4024576..71cd39953c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.Linq; -using Microsoft.AspNetCore.SignalR.Client; -using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -264,25 +262,13 @@ namespace osu.Game.Tests.Visual.Gameplay internal class TestReplayRecorder : ReplayRecorder { - private readonly SpectatorClient client; - public TestReplayRecorder(Replay target) : base(target) { - var connection = new HubConnectionBuilder() - .WithUrl("http://localhost:5009/spectator") - .AddMessagePackProtocol() - // .ConfigureLogging(logging => { logging.AddConsole(); }) - .Build(); - - connection.StartAsync().Wait(); - - client = new SpectatorClient(connection); } protected override ReplayFrame HandleFrame(Vector2 mousePosition, List actions, ReplayFrame previousFrame) { - client.SendFrames(new FrameDataBundle(new[] { new LegacyReplayFrame(Time.Current, mousePosition.X, mousePosition.Y, ReplayButtonState.None) })); return new TestReplayFrame(Time.Current, mousePosition, actions.ToArray()); } } diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs similarity index 58% rename from osu.Game/Online/Spectator/SpectatorClient.cs rename to osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 4558699618..a1a4a2774a 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -3,24 +3,70 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.DependencyInjection; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Online.API; namespace osu.Game.Online.Spectator { - public class SpectatorClient : ISpectatorClient + public class SpectatorStreamingClient : Component, ISpectatorClient { - private readonly HubConnection connection; + private HubConnection connection; private readonly List watchingUsers = new List(); - public SpectatorClient(HubConnection connection) - { - this.connection = connection; + private readonly IBindable apiState = new Bindable(); - // this is kind of SILLY - // https://github.com/dotnet/aspnetcore/issues/15198 + [Resolved] + private APIAccess api { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + apiState.BindTo(api.State); + apiState.BindValueChanged(apiStateChanged, true); + } + + private void apiStateChanged(ValueChangedEvent state) + { + switch (state.NewValue) + { + case APIState.Failing: + case APIState.Offline: + connection?.StopAsync(); + connection = null; + break; + + case APIState.Online: + connect(); + break; + } + } + +#if DEBUG + private const string endpoint = "http://localhost:5009/spectator"; +#else + private const string endpoint = "https://spectator.ppy.sh/spectator"; +#endif + + private void connect() + { + connection = new HubConnectionBuilder() + .WithUrl(endpoint, options => + { + options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); + }) + .AddMessagePackProtocol() + .Build(); + + // until strong typed client support is added, each method must be manually bound (see https://github.com/dotnet/aspnetcore/issues/15198) connection.On(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying); connection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames); connection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying); + + connection.StartAsync(); } Task ISpectatorClient.UserBeganPlaying(string userId, int beatmapId) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 2d609668af..9b43d18a88 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -30,6 +30,7 @@ using osu.Game.Database; using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.IO; +using osu.Game.Online.Spectator; using osu.Game.Overlays; using osu.Game.Resources; using osu.Game.Rulesets; @@ -74,6 +75,8 @@ namespace osu.Game protected IAPIProvider API; + private SpectatorStreamingClient spectatorStreaming; + protected MenuCursorContainer MenuCursorContainer; protected MusicController MusicController; @@ -189,9 +192,9 @@ namespace osu.Game dependencies.Cache(SkinManager = new SkinManager(Storage, contextFactory, Host, Audio, new NamespacedResourceStore(Resources, "Skins/Legacy"))); dependencies.CacheAs(SkinManager); - API ??= new APIAccess(LocalConfig); + dependencies.CacheAs(API ??= new APIAccess(LocalConfig)); - dependencies.CacheAs(API); + dependencies.CacheAs(spectatorStreaming = new SpectatorStreamingClient()); var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures); From 175fd512b00f26402023dcc21d36dfe9f0bddaee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 14:54:27 +0900 Subject: [PATCH 08/47] Send frames to streaming client from replay recorder --- .../Online/Spectator/SpectatorStreamingClient.cs | 13 +++++++++++++ osu.Game/OsuGameBase.cs | 3 +++ osu.Game/Rulesets/UI/ReplayRecorder.cs | 9 +++++++++ 3 files changed, 25 insertions(+) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index a1a4a2774a..c784eb09cd 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -7,7 +7,10 @@ using Microsoft.Extensions.DependencyInjection; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Beatmaps; using osu.Game.Online.API; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Replays.Types; namespace osu.Game.Online.Spectator { @@ -22,6 +25,9 @@ namespace osu.Game.Online.Spectator [Resolved] private APIAccess api { get; set; } + [Resolved] + private IBindable beatmap { get; set; } + [BackgroundDependencyLoader] private void load() { @@ -111,5 +117,12 @@ namespace osu.Game.Online.Spectator public Task EndPlaying(int beatmapId) => connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), beatmapId); private Task WatchUser(string userId) => connection.SendAsync(nameof(ISpectatorServer.StartWatchingUser), userId); + + public void HandleFrame(ReplayFrame frame) + { + if (frame is IConvertibleReplayFrame convertible) + // TODO: don't send a bundle for each individual frame + SendFrames(new FrameDataBundle(new[] { convertible.ToLegacy(beatmap.Value.Beatmap) })); + } } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 9b43d18a88..7364cf04b0 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -250,8 +250,11 @@ namespace osu.Game FileStore.Cleanup(); + // add api components to hierarchy. if (API is APIAccess apiAccess) AddInternal(apiAccess); + AddInternal(spectatorStreaming); + AddInternal(RulesetConfigCache); MenuCursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both }; diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index c977639584..3203d1afae 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -4,10 +4,12 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Online.Spectator; using osu.Game.Replays; using osu.Game.Rulesets.Replays; using osuTK; @@ -60,6 +62,9 @@ namespace osu.Game.Rulesets.UI recordFrame(true); } + [Resolved(canBeNull: true)] + private SpectatorStreamingClient spectatorStreaming { get; set; } + private void recordFrame(bool important) { var last = target.Frames.LastOrDefault(); @@ -72,7 +77,11 @@ namespace osu.Game.Rulesets.UI var frame = HandleFrame(position, pressedActions, last); if (frame != null) + { target.Frames.Add(frame); + + spectatorStreaming?.HandleFrame(frame); + } } protected abstract ReplayFrame HandleFrame(Vector2 mousePosition, List actions, ReplayFrame previousFrame); From 4788b4a643ac8b1465d200053aa6609ac7c8a679 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 15:03:43 +0900 Subject: [PATCH 09/47] Expose oauth access token via api interface --- osu.Game/Online/API/DummyAPIAccess.cs | 2 ++ osu.Game/Online/API/IAPIProvider.cs | 5 +++++ osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index da22a70bf8..e275676cea 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -20,6 +20,8 @@ namespace osu.Game.Online.API public Bindable Activity { get; } = new Bindable(); + public string AccessToken => "token"; + public bool IsLoggedIn => State.Value == APIState.Online; public string ProvidedUsername => LocalUser.Value.Username; diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 256d2ed151..9b7485decd 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -19,6 +19,11 @@ namespace osu.Game.Online.API /// Bindable Activity { get; } + /// + /// Retrieve the OAuth access token. + /// + public string AccessToken { get; } + /// /// Returns whether the local user is logged in. /// diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index c784eb09cd..a9a4987e69 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -23,7 +23,7 @@ namespace osu.Game.Online.Spectator private readonly IBindable apiState = new Bindable(); [Resolved] - private APIAccess api { get; set; } + private IAPIProvider api { get; set; } [Resolved] private IBindable beatmap { get; set; } From 96049c39c9f431f5eaa3709eb1332e83a9bb342b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 15:26:57 +0900 Subject: [PATCH 10/47] Add begin/end session logic --- osu.Game/Rulesets/UI/ReplayRecorder.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index 3203d1afae..c90b20caeb 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -5,10 +5,12 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Beatmaps; using osu.Game.Online.Spectator; using osu.Game.Replays; using osu.Game.Rulesets.Replays; @@ -27,6 +29,12 @@ namespace osu.Game.Rulesets.UI public int RecordFrameRate = 60; + [Resolved(canBeNull: true)] + private SpectatorStreamingClient spectatorStreaming { get; set; } + + [Resolved] + private IBindable beatmap { get; set; } + protected ReplayRecorder(Replay target) { this.target = target; @@ -41,6 +49,14 @@ namespace osu.Game.Rulesets.UI base.LoadComplete(); inputManager = GetContainingInputManager(); + + spectatorStreaming?.BeginPlaying(beatmap.Value.BeatmapInfo.ID); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + spectatorStreaming?.EndPlaying(beatmap.Value.BeatmapInfo.ID); } protected override bool OnMouseMove(MouseMoveEvent e) @@ -62,9 +78,6 @@ namespace osu.Game.Rulesets.UI recordFrame(true); } - [Resolved(canBeNull: true)] - private SpectatorStreamingClient spectatorStreaming { get; set; } - private void recordFrame(bool important) { var last = target.Frames.LastOrDefault(); From 2021945a8c87e4f8ec9a556705161f7e42b3c804 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 15:27:04 +0900 Subject: [PATCH 11/47] Add retry/error handling logic --- .../Spectator/SpectatorStreamingClient.cs | 67 ++++++++++++++++--- 1 file changed, 59 insertions(+), 8 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index a9a4987e69..2a19a665fc 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -22,6 +22,8 @@ namespace osu.Game.Online.Spectator private readonly IBindable apiState = new Bindable(); + private bool isConnected; + [Resolved] private IAPIProvider api { get; set; } @@ -46,7 +48,7 @@ namespace osu.Game.Online.Spectator break; case APIState.Online: - connect(); + Task.Run(connect); break; } } @@ -57,8 +59,11 @@ namespace osu.Game.Online.Spectator private const string endpoint = "https://spectator.ppy.sh/spectator"; #endif - private void connect() + private async Task connect() { + if (connection != null) + return; + connection = new HubConnectionBuilder() .WithUrl(endpoint, options => { @@ -72,7 +77,33 @@ namespace osu.Game.Online.Spectator connection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames); connection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying); - connection.StartAsync(); + connection.Closed += async ex => + { + isConnected = false; + if (ex != null) await tryUntilConnected(); + }; + + await tryUntilConnected(); + + async Task tryUntilConnected() + { + while (api.State.Value == APIState.Online) + { + try + { + // reconnect on any failure + await connection.StartAsync(); + + // success + isConnected = true; + break; + } + catch + { + await Task.Delay(5000); + } + } + } } Task ISpectatorClient.UserBeganPlaying(string userId, int beatmapId) @@ -106,17 +137,37 @@ namespace osu.Game.Online.Spectator Task ISpectatorClient.UserSentFrames(string userId, FrameDataBundle data) { - Console.WriteLine($"{connection.ConnectionId} Received frames from {userId}: {data.Frames.First().ToString()}"); + Console.WriteLine($"{connection.ConnectionId} Received frames from {userId}: {data.Frames.First()}"); return Task.CompletedTask; } - public Task BeginPlaying(int beatmapId) => connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), beatmapId); + public void BeginPlaying(int beatmapId) + { + if (!isConnected) return; - public Task SendFrames(FrameDataBundle data) => connection.SendAsync(nameof(ISpectatorServer.SendFrameData), data); + connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), beatmapId); + } - public Task EndPlaying(int beatmapId) => connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), beatmapId); + public void SendFrames(FrameDataBundle data) + { + if (!isConnected) return; - private Task WatchUser(string userId) => connection.SendAsync(nameof(ISpectatorServer.StartWatchingUser), userId); + connection.SendAsync(nameof(ISpectatorServer.SendFrameData), data); + } + + public void EndPlaying(int beatmapId) + { + if (!isConnected) return; + + connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), beatmapId); + } + + public void WatchUser(string userId) + { + if (!isConnected) return; + + connection.SendAsync(nameof(ISpectatorServer.StartWatchingUser), userId); + } public void HandleFrame(ReplayFrame frame) { From 05697dfe68a58d5a2e2c780a80bb562e31fcd923 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 17:29:38 +0900 Subject: [PATCH 12/47] Add spectator state object support --- osu.Game/Online/Spectator/ISpectatorClient.cs | 8 ++--- osu.Game/Online/Spectator/ISpectatorServer.cs | 9 +++--- osu.Game/Online/Spectator/SpectatorState.cs | 32 +++++++++++++++++++ .../Spectator/SpectatorStreamingClient.cs | 23 ++++++++----- osu.Game/Rulesets/UI/ReplayRecorder.cs | 4 +-- 5 files changed, 57 insertions(+), 19 deletions(-) create mode 100644 osu.Game/Online/Spectator/SpectatorState.cs diff --git a/osu.Game/Online/Spectator/ISpectatorClient.cs b/osu.Game/Online/Spectator/ISpectatorClient.cs index ed762ac1fe..dcff6e6c1c 100644 --- a/osu.Game/Online/Spectator/ISpectatorClient.cs +++ b/osu.Game/Online/Spectator/ISpectatorClient.cs @@ -11,15 +11,15 @@ namespace osu.Game.Online.Spectator /// Signals that a user has begun a new play session. /// /// The user. - /// The beatmap the user is playing. - Task UserBeganPlaying(string userId, int beatmapId); + /// The state of gameplay. + Task UserBeganPlaying(string userId, SpectatorState state); /// /// Signals that a user has finished a play session. /// /// The user. - /// The beatmap the user has finished playing. - Task UserFinishedPlaying(string userId, int beatmapId); + /// The state of gameplay. + Task UserFinishedPlaying(string userId, SpectatorState state); /// /// Called when new frames are available for a subscribed user's play session. diff --git a/osu.Game/Online/Spectator/ISpectatorServer.cs b/osu.Game/Online/Spectator/ISpectatorServer.cs index 03ca37d524..018fa6b66b 100644 --- a/osu.Game/Online/Spectator/ISpectatorServer.cs +++ b/osu.Game/Online/Spectator/ISpectatorServer.cs @@ -10,8 +10,8 @@ namespace osu.Game.Online.Spectator /// /// Signal the start of a new play session. /// - /// The beatmap currently being played. Eventually this should be replaced with more complete metadata. - Task BeginPlaySession(int beatmapId); + /// The state of gameplay. + Task BeginPlaySession(SpectatorState state); /// /// Send a bundle of frame data for the current play session. @@ -22,15 +22,14 @@ namespace osu.Game.Online.Spectator /// /// Signal the end of a play session. /// - /// The beatmap that was completed. This should be replaced with a play token once that flow is established. - Task EndPlaySession(int beatmapId); + /// The state of gameplay. + Task EndPlaySession(SpectatorState state); /// /// Request spectating data for the specified user. May be called on multiple users and offline users. /// For offline users, a subscription will be created and data will begin streaming on next play. /// /// The user to subscribe to. - /// Task StartWatchingUser(string userId); /// diff --git a/osu.Game/Online/Spectator/SpectatorState.cs b/osu.Game/Online/Spectator/SpectatorState.cs new file mode 100644 index 0000000000..90238bfc38 --- /dev/null +++ b/osu.Game/Online/Spectator/SpectatorState.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Online.Spectator +{ + [Serializable] + public class SpectatorState : IEquatable + { + public int? BeatmapID { get; set; } + + [NotNull] + public IEnumerable Mods { get; set; } = Enumerable.Empty(); + + public SpectatorState(int? beatmapId = null, IEnumerable mods = null) + { + BeatmapID = beatmapId; + if (mods != null) + Mods = mods; + } + + public SpectatorState() + { + } + + public bool Equals(SpectatorState other) => this.BeatmapID == other?.BeatmapID && this.Mods.SequenceEqual(other?.Mods); + + public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods.SelectMany(m => m.Acronym))}"; + } +} diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 2a19a665fc..d93de3a710 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Online.API; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; @@ -30,6 +31,11 @@ namespace osu.Game.Online.Spectator [Resolved] private IBindable beatmap { get; set; } + [Resolved] + private IBindable> mods { get; set; } + + private readonly SpectatorState currentState = new SpectatorState(); + [BackgroundDependencyLoader] private void load() { @@ -73,9 +79,9 @@ namespace osu.Game.Online.Spectator .Build(); // until strong typed client support is added, each method must be manually bound (see https://github.com/dotnet/aspnetcore/issues/15198) - connection.On(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying); + connection.On(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying); connection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames); - connection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying); + connection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying); connection.Closed += async ex => { @@ -106,7 +112,7 @@ namespace osu.Game.Online.Spectator } } - Task ISpectatorClient.UserBeganPlaying(string userId, int beatmapId) + Task ISpectatorClient.UserBeganPlaying(string userId, SpectatorState state) { if (connection.ConnectionId != userId) { @@ -123,15 +129,15 @@ namespace osu.Game.Online.Spectator } else { - Console.WriteLine($"{connection.ConnectionId} Received user playing event for self {beatmapId}"); + Console.WriteLine($"{connection.ConnectionId} Received user playing event for self {state}"); } return Task.CompletedTask; } - Task ISpectatorClient.UserFinishedPlaying(string userId, int beatmapId) + Task ISpectatorClient.UserFinishedPlaying(string userId, SpectatorState state) { - Console.WriteLine($"{connection.ConnectionId} Received user finished event {beatmapId}"); + Console.WriteLine($"{connection.ConnectionId} Received user finished event {state}"); return Task.CompletedTask; } @@ -155,11 +161,11 @@ namespace osu.Game.Online.Spectator connection.SendAsync(nameof(ISpectatorServer.SendFrameData), data); } - public void EndPlaying(int beatmapId) + public void EndPlaying() { if (!isConnected) return; - connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), beatmapId); + connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), currentState); } public void WatchUser(string userId) @@ -171,6 +177,7 @@ namespace osu.Game.Online.Spectator public void HandleFrame(ReplayFrame frame) { + // ReSharper disable once SuspiciousTypeConversion.Global (implemented by rulesets) if (frame is IConvertibleReplayFrame convertible) // TODO: don't send a bundle for each individual frame SendFrames(new FrameDataBundle(new[] { convertible.ToLegacy(beatmap.Value.Beatmap) })); diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index c90b20caeb..a84b4f4ba8 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -50,13 +50,13 @@ namespace osu.Game.Rulesets.UI inputManager = GetContainingInputManager(); - spectatorStreaming?.BeginPlaying(beatmap.Value.BeatmapInfo.ID); + spectatorStreaming?.BeginPlaying(); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - spectatorStreaming?.EndPlaying(beatmap.Value.BeatmapInfo.ID); + spectatorStreaming?.EndPlaying(); } protected override bool OnMouseMove(MouseMoveEvent e) From 0611b30258e1c5c5dcb2c5b346c4658a3773f69a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 17:29:43 +0900 Subject: [PATCH 13/47] Drop webpack --- osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 11 ++++++++--- osu.Game/Replays/Legacy/LegacyReplayFrame.cs | 2 -- osu.Game/osu.Game.csproj | 3 +-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index d93de3a710..a89cc82535 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -75,7 +76,7 @@ namespace osu.Game.Online.Spectator { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); }) - .AddMessagePackProtocol() + .AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; }) .Build(); // until strong typed client support is added, each method must be manually bound (see https://github.com/dotnet/aspnetcore/issues/15198) @@ -147,11 +148,15 @@ namespace osu.Game.Online.Spectator return Task.CompletedTask; } - public void BeginPlaying(int beatmapId) + public void BeginPlaying() { if (!isConnected) return; - connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), beatmapId); + // transfer state at point of beginning play + currentState.BeatmapID = beatmap.Value.BeatmapInfo.OnlineBeatmapID; + currentState.Mods = mods.Value.ToArray(); + + connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), currentState); } public void SendFrames(FrameDataBundle data) diff --git a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs index 656fd1814e..c3cffa8699 100644 --- a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs +++ b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using MessagePack; using osu.Game.Rulesets.Replays; using osuTK; @@ -9,7 +8,6 @@ namespace osu.Game.Replays.Legacy { public class LegacyReplayFrame : ReplayFrame { - [IgnoreMember] public Vector2 Position => new Vector2(MouseX ?? 0, MouseY ?? 0); public float? MouseX; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index fd010fcc43..ca588b89d9 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -21,9 +21,8 @@ - - + From c834aa605103b95cd9e074b6bd55ac4861694181 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 17:38:16 +0900 Subject: [PATCH 14/47] Use APIMod for mod serialization --- osu.Game/Online/API/APIMod.cs | 8 ++++++++ osu.Game/Online/Spectator/SpectatorState.cs | 11 ++++------- osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/osu.Game/Online/API/APIMod.cs b/osu.Game/Online/API/APIMod.cs index 46a8db31b7..780e5daa16 100644 --- a/osu.Game/Online/API/APIMod.cs +++ b/osu.Game/Online/API/APIMod.cs @@ -53,5 +53,13 @@ namespace osu.Game.Online.API } public bool Equals(IMod other) => Acronym == other?.Acronym; + + public override string ToString() + { + if (Settings.Count > 0) + return $"{Acronym} ({string.Join(',', Settings.Select(kvp => $"{kvp.Key}:{kvp.Value}"))})"; + + return $"{Acronym}"; + } } } diff --git a/osu.Game/Online/Spectator/SpectatorState.cs b/osu.Game/Online/Spectator/SpectatorState.cs index 90238bfc38..3d9997f006 100644 --- a/osu.Game/Online/Spectator/SpectatorState.cs +++ b/osu.Game/Online/Spectator/SpectatorState.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using osu.Game.Online.API; using osu.Game.Rulesets.Mods; namespace osu.Game.Online.Spectator @@ -12,21 +13,17 @@ namespace osu.Game.Online.Spectator public int? BeatmapID { get; set; } [NotNull] - public IEnumerable Mods { get; set; } = Enumerable.Empty(); + public IEnumerable Mods { get; set; } = Enumerable.Empty(); - public SpectatorState(int? beatmapId = null, IEnumerable mods = null) + public SpectatorState(int? beatmapId = null, IEnumerable mods = null) { BeatmapID = beatmapId; if (mods != null) Mods = mods; } - public SpectatorState() - { - } - public bool Equals(SpectatorState other) => this.BeatmapID == other?.BeatmapID && this.Mods.SequenceEqual(other?.Mods); - public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods.SelectMany(m => m.Acronym))}"; + public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)}"; } } diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index a89cc82535..21259bad5f 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -154,7 +154,7 @@ namespace osu.Game.Online.Spectator // transfer state at point of beginning play currentState.BeatmapID = beatmap.Value.BeatmapInfo.OnlineBeatmapID; - currentState.Mods = mods.Value.ToArray(); + currentState.Mods = mods.Value.Select(m => new APIMod(m)); connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), currentState); } From 1ab6f41b3ba0127db8ae00609a821fbea36ce26a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 18:10:27 +0900 Subject: [PATCH 15/47] Add basic send and receive test --- .../Visual/Gameplay/TestSceneSpectator.cs | 264 ++++++++++++++++++ .../Spectator/SpectatorStreamingClient.cs | 3 + 2 files changed, 267 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs new file mode 100644 index 0000000000..665df5f9c7 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -0,0 +1,264 @@ +// 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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Framework.Input.StateChanges; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Spectator; +using osu.Game.Replays; +using osu.Game.Replays.Legacy; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Replays.Types; +using osu.Game.Rulesets.UI; +using osu.Game.Tests.Visual.UserInterface; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneSpectator : OsuManualInputManagerTestScene + { + protected override bool UseOnlineAPI => true; + + private TestRulesetInputManager playbackManager; + private TestRulesetInputManager recordingManager; + + private Replay replay; + + private TestReplayRecorder recorder; + + [Resolved] + private SpectatorStreamingClient streamingClient { get; set; } + + [SetUp] + public void SetUp() => Schedule(() => + { + replay = new Replay(); + + streamingClient.OnNewFrames += frames => + { + foreach (var legacyFrame in frames.Frames) + { + var frame = new TestReplayFrame(); + frame.FromLegacy(legacyFrame, null, null); + replay.Frames.Add(frame); + } + }; + + Add(new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + recordingManager = new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique) + { + Recorder = recorder = new TestReplayRecorder + { + ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos), + }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Brown, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Text = "Sending", + Scale = new Vector2(3), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new TestInputConsumer() + } + }, + } + }, + new Drawable[] + { + playbackManager = new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique) + { + ReplayInputHandler = new TestFramedReplayInputHandler(replay) + { + GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos), + }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = Color4.DarkBlue, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Text = "Receiving", + Scale = new Vector2(3), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new TestInputConsumer() + } + }, + } + } + } + }); + }); + + [Test] + public void TestBasic() + { + } + + protected override void Update() + { + base.Update(); + playbackManager?.ReplayInputHandler.SetFrameFromTime(Time.Current - 100); + } + + public class TestFramedReplayInputHandler : FramedReplayInputHandler + { + public TestFramedReplayInputHandler(Replay replay) + : base(replay) + { + } + + public override void CollectPendingInputs(List inputs) + { + inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) }); + inputs.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() }); + } + } + + public class TestInputConsumer : CompositeDrawable, IKeyBindingHandler + { + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent.ReceivePositionalInputAt(screenSpacePos); + + private readonly Box box; + + public TestInputConsumer() + { + Size = new Vector2(30); + + Origin = Anchor.Centre; + + InternalChildren = new Drawable[] + { + box = new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + }, + }; + } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + Position = e.MousePosition; + return base.OnMouseMove(e); + } + + public bool OnPressed(TestAction action) + { + box.Colour = Color4.White; + return true; + } + + public void OnReleased(TestAction action) + { + box.Colour = Color4.Black; + } + } + + public class TestRulesetInputManager : RulesetInputManager + { + public TestRulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) + : base(ruleset, variant, unique) + { + } + + protected override KeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) + => new TestKeyBindingContainer(); + + internal class TestKeyBindingContainer : KeyBindingContainer + { + public override IEnumerable DefaultKeyBindings => new[] + { + new KeyBinding(InputKey.MouseLeft, TestAction.Down), + }; + } + } + + public class TestReplayFrame : ReplayFrame, IConvertibleReplayFrame + { + public Vector2 Position; + + public List Actions = new List(); + + public TestReplayFrame(double time, Vector2 position, params TestAction[] actions) + : base(time) + { + Position = position; + Actions.AddRange(actions); + } + + public TestReplayFrame() + { + } + + public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null) + { + Position = currentFrame.Position; + Time = currentFrame.Time; + if (currentFrame.MouseLeft) + Actions.Add(TestAction.Down); + } + + public LegacyReplayFrame ToLegacy(IBeatmap beatmap) + { + ReplayButtonState state = ReplayButtonState.None; + + if (Actions.Contains(TestAction.Down)) + state |= ReplayButtonState.Left1; + + return new LegacyReplayFrame(Time, Position.X, Position.Y, state); + } + } + + public enum TestAction + { + Down, + } + + internal class TestReplayRecorder : ReplayRecorder + { + public TestReplayRecorder() + : base(new Replay()) + { + } + + protected override ReplayFrame HandleFrame(Vector2 mousePosition, List actions, ReplayFrame previousFrame) + { + return new TestReplayFrame(Time.Current, mousePosition, actions.ToArray()); + } + } + } +} diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 21259bad5f..608123fbab 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -37,6 +37,8 @@ namespace osu.Game.Online.Spectator private readonly SpectatorState currentState = new SpectatorState(); + public event Action OnNewFrames; + [BackgroundDependencyLoader] private void load() { @@ -145,6 +147,7 @@ namespace osu.Game.Online.Spectator Task ISpectatorClient.UserSentFrames(string userId, FrameDataBundle data) { Console.WriteLine($"{connection.ConnectionId} Received frames from {userId}: {data.Frames.First()}"); + OnNewFrames?.Invoke(data); return Task.CompletedTask; } From 34e889e66e3bfeabd22ea5eb6550b5e338c15bce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 18:37:19 +0900 Subject: [PATCH 16/47] Don't watch every user in normal gameplay (but allow so in test) --- .../Visual/Gameplay/TestSceneSpectator.cs | 20 ++++- osu.Game/Online/Spectator/ISpectatorClient.cs | 6 +- osu.Game/Online/Spectator/ISpectatorServer.cs | 4 +- osu.Game/Online/Spectator/SpectatorState.cs | 1 - .../Spectator/SpectatorStreamingClient.cs | 85 ++++++++++++------- 5 files changed, 78 insertions(+), 38 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 665df5f9c7..2ec82ad5fb 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -2,8 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Collections.Specialized; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -34,7 +36,7 @@ namespace osu.Game.Tests.Visual.Gameplay private Replay replay; - private TestReplayRecorder recorder; + private IBindableList users; [Resolved] private SpectatorStreamingClient streamingClient { get; set; } @@ -44,7 +46,19 @@ namespace osu.Game.Tests.Visual.Gameplay { replay = new Replay(); - streamingClient.OnNewFrames += frames => + users = streamingClient.PlayingUsers.GetBoundCopy(); + users.BindCollectionChanged((obj, args) => + { + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + foreach (int user in args.NewItems) + streamingClient.WatchUser(user); + break; + } + }, true); + + streamingClient.OnNewFrames += (userId, frames) => { foreach (var legacyFrame in frames.Frames) { @@ -63,7 +77,7 @@ namespace osu.Game.Tests.Visual.Gameplay { recordingManager = new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique) { - Recorder = recorder = new TestReplayRecorder + Recorder = new TestReplayRecorder { ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos), }, diff --git a/osu.Game/Online/Spectator/ISpectatorClient.cs b/osu.Game/Online/Spectator/ISpectatorClient.cs index dcff6e6c1c..18c9d61561 100644 --- a/osu.Game/Online/Spectator/ISpectatorClient.cs +++ b/osu.Game/Online/Spectator/ISpectatorClient.cs @@ -12,20 +12,20 @@ namespace osu.Game.Online.Spectator /// /// The user. /// The state of gameplay. - Task UserBeganPlaying(string userId, SpectatorState state); + Task UserBeganPlaying(int userId, SpectatorState state); /// /// Signals that a user has finished a play session. /// /// The user. /// The state of gameplay. - Task UserFinishedPlaying(string userId, SpectatorState state); + Task UserFinishedPlaying(int userId, SpectatorState state); /// /// Called when new frames are available for a subscribed user's play session. /// /// The user. /// The frame data. - Task UserSentFrames(string userId, FrameDataBundle data); + Task UserSentFrames(int userId, FrameDataBundle data); } } diff --git a/osu.Game/Online/Spectator/ISpectatorServer.cs b/osu.Game/Online/Spectator/ISpectatorServer.cs index 018fa6b66b..99893e385c 100644 --- a/osu.Game/Online/Spectator/ISpectatorServer.cs +++ b/osu.Game/Online/Spectator/ISpectatorServer.cs @@ -30,12 +30,12 @@ namespace osu.Game.Online.Spectator /// For offline users, a subscription will be created and data will begin streaming on next play. /// /// The user to subscribe to. - Task StartWatchingUser(string userId); + Task StartWatchingUser(int userId); /// /// Stop requesting spectating data for the specified user. Unsubscribes from receiving further data. /// /// The user to unsubscribe from. - Task EndWatchingUser(string userId); + Task EndWatchingUser(int userId); } } diff --git a/osu.Game/Online/Spectator/SpectatorState.cs b/osu.Game/Online/Spectator/SpectatorState.cs index 3d9997f006..6b2b8b8cb2 100644 --- a/osu.Game/Online/Spectator/SpectatorState.cs +++ b/osu.Game/Online/Spectator/SpectatorState.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using osu.Game.Online.API; -using osu.Game.Rulesets.Mods; namespace osu.Game.Online.Spectator { diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 608123fbab..2665243e4c 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; @@ -20,7 +21,11 @@ namespace osu.Game.Online.Spectator { private HubConnection connection; - private readonly List watchingUsers = new List(); + private readonly List watchingUsers = new List(); + + public IBindableList PlayingUsers => playingUsers; + + private readonly BindableList playingUsers = new BindableList(); private readonly IBindable apiState = new Bindable(); @@ -37,7 +42,12 @@ namespace osu.Game.Online.Spectator private readonly SpectatorState currentState = new SpectatorState(); - public event Action OnNewFrames; + private bool isPlaying; + + /// + /// Called whenever new frames arrive from the server. + /// + public event Action OnNewFrames; [BackgroundDependencyLoader] private void load() @@ -82,13 +92,15 @@ namespace osu.Game.Online.Spectator .Build(); // until strong typed client support is added, each method must be manually bound (see https://github.com/dotnet/aspnetcore/issues/15198) - connection.On(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying); - connection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames); - connection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying); + connection.On(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying); + connection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames); + connection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying); connection.Closed += async ex => { isConnected = false; + playingUsers.Clear(); + if (ex != null) await tryUntilConnected(); }; @@ -105,6 +117,17 @@ namespace osu.Game.Online.Spectator // success isConnected = true; + + // resubscribe to watched users + var users = watchingUsers.ToArray(); + watchingUsers.Clear(); + foreach (var userId in users) + WatchUser(userId); + + // re-send state in case it wasn't received + if (isPlaying) + beginPlaying(); + break; } catch @@ -115,39 +138,23 @@ namespace osu.Game.Online.Spectator } } - Task ISpectatorClient.UserBeganPlaying(string userId, SpectatorState state) + Task ISpectatorClient.UserBeganPlaying(int userId, SpectatorState state) { - if (connection.ConnectionId != userId) - { - if (watchingUsers.Contains(userId)) - { - Console.WriteLine($"{connection.ConnectionId} received began playing for already watched user {userId}"); - } - else - { - Console.WriteLine($"{connection.ConnectionId} requesting watch other user {userId}"); - WatchUser(userId); - watchingUsers.Add(userId); - } - } - else - { - Console.WriteLine($"{connection.ConnectionId} Received user playing event for self {state}"); - } + if (!playingUsers.Contains(userId)) + playingUsers.Add(userId); return Task.CompletedTask; } - Task ISpectatorClient.UserFinishedPlaying(string userId, SpectatorState state) + Task ISpectatorClient.UserFinishedPlaying(int userId, SpectatorState state) { - Console.WriteLine($"{connection.ConnectionId} Received user finished event {state}"); + playingUsers.Remove(userId); return Task.CompletedTask; } - Task ISpectatorClient.UserSentFrames(string userId, FrameDataBundle data) + Task ISpectatorClient.UserSentFrames(int userId, FrameDataBundle data) { - Console.WriteLine($"{connection.ConnectionId} Received frames from {userId}: {data.Frames.First()}"); - OnNewFrames?.Invoke(data); + OnNewFrames?.Invoke(userId, data); return Task.CompletedTask; } @@ -155,10 +162,22 @@ namespace osu.Game.Online.Spectator { if (!isConnected) return; + if (isPlaying) + throw new InvalidOperationException($"Cannot invoke {nameof(BeginPlaying)} when already playing"); + + isPlaying = true; + // transfer state at point of beginning play currentState.BeatmapID = beatmap.Value.BeatmapInfo.OnlineBeatmapID; currentState.Mods = mods.Value.Select(m => new APIMod(m)); + beginPlaying(); + } + + private void beginPlaying() + { + Debug.Assert(isPlaying); + connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), currentState); } @@ -173,13 +192,21 @@ namespace osu.Game.Online.Spectator { if (!isConnected) return; + if (!isPlaying) + throw new InvalidOperationException($"Cannot invoke {nameof(EndPlaying)} when not playing"); + + isPlaying = false; connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), currentState); } - public void WatchUser(string userId) + public void WatchUser(int userId) { if (!isConnected) return; + if (watchingUsers.Contains(userId)) + return; + + watchingUsers.Add(userId); connection.SendAsync(nameof(ISpectatorServer.StartWatchingUser), userId); } From d659b7739d4c78ff8926565145c1952fdb91b76f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 19:16:34 +0900 Subject: [PATCH 17/47] Correctly stop watching users that leave --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 2ec82ad5fb..be3241c784 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -55,6 +55,11 @@ namespace osu.Game.Tests.Visual.Gameplay foreach (int user in args.NewItems) streamingClient.WatchUser(user); break; + + case NotifyCollectionChangedAction.Remove: + foreach (int user in args.OldItems) + streamingClient.StopWatchingUser(user); + break; } }, true); From 823d717a7d986e5551b50d87f1d3abefdffb560b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 19:17:10 +0900 Subject: [PATCH 18/47] Reduce the serialised size of LegacyReplayFrame --- osu.Game/Replays/Legacy/LegacyReplayFrame.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs index c3cffa8699..74bacae9e1 100644 --- a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs +++ b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using Newtonsoft.Json; using osu.Game.Rulesets.Replays; using osuTK; @@ -8,17 +9,28 @@ namespace osu.Game.Replays.Legacy { public class LegacyReplayFrame : ReplayFrame { + [JsonIgnore] public Vector2 Position => new Vector2(MouseX ?? 0, MouseY ?? 0); public float? MouseX; public float? MouseY; + [JsonIgnore] public bool MouseLeft => MouseLeft1 || MouseLeft2; + + [JsonIgnore] public bool MouseRight => MouseRight1 || MouseRight2; + [JsonIgnore] public bool MouseLeft1 => ButtonState.HasFlag(ReplayButtonState.Left1); + + [JsonIgnore] public bool MouseRight1 => ButtonState.HasFlag(ReplayButtonState.Right1); + + [JsonIgnore] public bool MouseLeft2 => ButtonState.HasFlag(ReplayButtonState.Left2); + + [JsonIgnore] public bool MouseRight2 => ButtonState.HasFlag(ReplayButtonState.Right2); public ReplayButtonState ButtonState; From ee2513bf4b7fc17192dc584c21f1f1a911c59971 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 19:17:19 +0900 Subject: [PATCH 19/47] Add batch sending --- .../Spectator/SpectatorStreamingClient.cs | 56 +++++++++++++++++-- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 2665243e4c..9ebb84c007 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -11,6 +11,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Online.API; +using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; @@ -185,7 +186,7 @@ namespace osu.Game.Online.Spectator { if (!isConnected) return; - connection.SendAsync(nameof(ISpectatorServer.SendFrameData), data); + lastSend = connection.SendAsync(nameof(ISpectatorServer.SendFrameData), data); } public void EndPlaying() @@ -201,21 +202,64 @@ namespace osu.Game.Online.Spectator public void WatchUser(int userId) { - if (!isConnected) return; - if (watchingUsers.Contains(userId)) return; watchingUsers.Add(userId); + + if (!isConnected) return; + connection.SendAsync(nameof(ISpectatorServer.StartWatchingUser), userId); } + public void StopWatchingUser(int userId) + { + watchingUsers.Remove(userId); + + if (!isConnected) return; + + connection.SendAsync(nameof(ISpectatorServer.EndWatchingUser), userId); + } + + private readonly Queue pendingFrames = new Queue(); + + private double lastSendTime; + + private Task lastSend; + + private const double time_between_sends = 200; + + private const int max_pending_frames = 30; + + protected override void Update() + { + base.Update(); + + if (pendingFrames.Count > 0 && Time.Current - lastSendTime > time_between_sends) + purgePendingFrames(); + } + public void HandleFrame(ReplayFrame frame) { - // ReSharper disable once SuspiciousTypeConversion.Global (implemented by rulesets) if (frame is IConvertibleReplayFrame convertible) - // TODO: don't send a bundle for each individual frame - SendFrames(new FrameDataBundle(new[] { convertible.ToLegacy(beatmap.Value.Beatmap) })); + pendingFrames.Enqueue(convertible.ToLegacy(beatmap.Value.Beatmap)); + + if (pendingFrames.Count > max_pending_frames) + purgePendingFrames(); + } + + private void purgePendingFrames() + { + if (lastSend?.IsCompleted == false) + return; + + var frames = pendingFrames.ToArray(); + + pendingFrames.Clear(); + + SendFrames(new FrameDataBundle(frames)); + + lastSendTime = Time.Current; } } } From 04f46bc1f84739780214d468f1d79a298ffd4ee3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 19:24:32 +0900 Subject: [PATCH 20/47] Clean up usings --- osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index 71cd39953c..d464eee7c5 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -13,9 +13,7 @@ using osu.Framework.Input.StateChanges; using osu.Framework.Testing; using osu.Framework.Threading; using osu.Game.Graphics.Sprites; -using osu.Game.Online.Spectator; using osu.Game.Replays; -using osu.Game.Replays.Legacy; using osu.Game.Rulesets; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.UI; From 147d502da13bb303af11cf53395cd53ed09f4e8c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 19:30:07 +0900 Subject: [PATCH 21/47] Fix initial play state not being kept locally if not connected --- osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 9ebb84c007..6737625818 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -161,8 +161,6 @@ namespace osu.Game.Online.Spectator public void BeginPlaying() { - if (!isConnected) return; - if (isPlaying) throw new InvalidOperationException($"Cannot invoke {nameof(BeginPlaying)} when already playing"); @@ -179,6 +177,8 @@ namespace osu.Game.Online.Spectator { Debug.Assert(isPlaying); + if (!isConnected) return; + connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), currentState); } From 51ae93d484f31fd586fddee4095657caaf0924ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 19:31:56 +0900 Subject: [PATCH 22/47] Revert unnecessary file changes --- osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index d464eee7c5..bc1c10e59d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -266,9 +266,7 @@ namespace osu.Game.Tests.Visual.Gameplay } protected override ReplayFrame HandleFrame(Vector2 mousePosition, List actions, ReplayFrame previousFrame) - { - return new TestReplayFrame(Time.Current, mousePosition, actions.ToArray()); - } + => new TestReplayFrame(Time.Current, mousePosition, actions.ToArray()); } } } From 9f2f8d8cc778df600182d50defdf28ae5106f0ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 19:41:10 +0900 Subject: [PATCH 23/47] Fix missing licence headers --- osu.Game/Online/Spectator/FrameDataBundle.cs | 3 +++ osu.Game/Online/Spectator/ISpectatorClient.cs | 3 +++ osu.Game/Online/Spectator/ISpectatorServer.cs | 3 +++ osu.Game/Online/Spectator/SpectatorState.cs | 5 ++++- osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 3 +++ 5 files changed, 16 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Spectator/FrameDataBundle.cs b/osu.Game/Online/Spectator/FrameDataBundle.cs index 67f2688289..5281e61f9c 100644 --- a/osu.Game/Online/Spectator/FrameDataBundle.cs +++ b/osu.Game/Online/Spectator/FrameDataBundle.cs @@ -1,3 +1,6 @@ +// 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 System.Collections.Generic; using osu.Game.Replays.Legacy; diff --git a/osu.Game/Online/Spectator/ISpectatorClient.cs b/osu.Game/Online/Spectator/ISpectatorClient.cs index 18c9d61561..3acc9b2282 100644 --- a/osu.Game/Online/Spectator/ISpectatorClient.cs +++ b/osu.Game/Online/Spectator/ISpectatorClient.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using System.Threading.Tasks; namespace osu.Game.Online.Spectator diff --git a/osu.Game/Online/Spectator/ISpectatorServer.cs b/osu.Game/Online/Spectator/ISpectatorServer.cs index 99893e385c..af0196862a 100644 --- a/osu.Game/Online/Spectator/ISpectatorServer.cs +++ b/osu.Game/Online/Spectator/ISpectatorServer.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using System.Threading.Tasks; namespace osu.Game.Online.Spectator diff --git a/osu.Game/Online/Spectator/SpectatorState.cs b/osu.Game/Online/Spectator/SpectatorState.cs index 6b2b8b8cb2..48fad4b3b2 100644 --- a/osu.Game/Online/Spectator/SpectatorState.cs +++ b/osu.Game/Online/Spectator/SpectatorState.cs @@ -1,3 +1,6 @@ +// 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 System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -21,7 +24,7 @@ namespace osu.Game.Online.Spectator Mods = mods; } - public bool Equals(SpectatorState other) => this.BeatmapID == other?.BeatmapID && this.Mods.SequenceEqual(other?.Mods); + public bool Equals(SpectatorState other) => BeatmapID == other?.BeatmapID && Mods.SequenceEqual(other?.Mods); public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)}"; } diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 6737625818..006f75c1d2 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -1,3 +1,6 @@ +// 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 System.Collections.Generic; using System.Diagnostics; From 54d666604bc0daa207aa1c923715a391b22badb4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 22:56:23 +0900 Subject: [PATCH 24/47] Fix incorrect order of flag settings --- osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 006f75c1d2..2fc1431702 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -194,12 +194,13 @@ namespace osu.Game.Online.Spectator public void EndPlaying() { - if (!isConnected) return; - if (!isPlaying) throw new InvalidOperationException($"Cannot invoke {nameof(EndPlaying)} when not playing"); isPlaying = false; + + if (!isConnected) return; + connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), currentState); } From f11bcfcb8f301b8d19e465176a7b66ca70f88858 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Oct 2020 10:03:33 +0900 Subject: [PATCH 25/47] Remove unnecessary public specification in interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Online/API/IAPIProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 9b7485decd..d10cb4b6d2 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -22,7 +22,7 @@ namespace osu.Game.Online.API /// /// Retrieve the OAuth access token. /// - public string AccessToken { get; } + string AccessToken { get; } /// /// Returns whether the local user is logged in. From e99cf369fac8e35b16a2b9458651f847cb1905f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Oct 2020 13:33:23 +0900 Subject: [PATCH 26/47] Don't worry about EndPlaying being invoked when not playing --- osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 2fc1431702..1ca0a378bb 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -194,9 +194,6 @@ namespace osu.Game.Online.Spectator public void EndPlaying() { - if (!isPlaying) - throw new InvalidOperationException($"Cannot invoke {nameof(EndPlaying)} when not playing"); - isPlaying = false; if (!isConnected) return; From 55f1b05dbf6c74e389cfc6af2133ed03bd7e2da0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Oct 2020 14:47:08 +0900 Subject: [PATCH 27/47] Fix test failures due to recorder not stopping in time --- .../Visual/Gameplay/TestSceneReplayRecorder.cs | 6 ++++++ osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 11 ++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index bc1c10e59d..e964d2a40e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -166,6 +166,12 @@ namespace osu.Game.Tests.Visual.Gameplay playbackManager?.ReplayInputHandler.SetFrameFromTime(Time.Current - 100); } + [TearDownSteps] + public void TearDown() + { + AddStep("stop recorder", () => recorder.Expire()); + } + public class TestFramedReplayInputHandler : FramedReplayInputHandler { public TestFramedReplayInputHandler(Replay replay) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index be3241c784..f8b5d385a9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Input.StateChanges; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Online.Spectator; @@ -38,6 +39,8 @@ namespace osu.Game.Tests.Visual.Gameplay private IBindableList users; + private TestReplayRecorder recorder; + [Resolved] private SpectatorStreamingClient streamingClient { get; set; } @@ -82,7 +85,7 @@ namespace osu.Game.Tests.Visual.Gameplay { recordingManager = new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique) { - Recorder = new TestReplayRecorder + Recorder = recorder = new TestReplayRecorder { ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos), }, @@ -153,6 +156,12 @@ namespace osu.Game.Tests.Visual.Gameplay playbackManager?.ReplayInputHandler.SetFrameFromTime(Time.Current - 100); } + [TearDownSteps] + public void TearDown() + { + AddStep("stop recorder", () => recorder.Expire()); + } + public class TestFramedReplayInputHandler : FramedReplayInputHandler { public TestFramedReplayInputHandler(Replay replay) From 4fca7675b07fbd9c9784560ec22479cc986c0223 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Oct 2020 14:47:21 +0900 Subject: [PATCH 28/47] Don't send spectate data when an autoplay mod is active --- osu.Game/Screens/Play/Player.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 9ee0b8a54f..6b2d2f40d0 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -152,7 +152,9 @@ namespace osu.Game.Screens.Play { base.LoadComplete(); - PrepareReplay(); + // replays should never be recorded or played back when autoplay is enabled + if (!Mods.Value.Any(m => m is ModAutoplay)) + PrepareReplay(); } private Replay recordingReplay; From e20a98640199bce08ba1f445ca283027f8fe9282 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Oct 2020 17:24:19 +0900 Subject: [PATCH 29/47] Add ruleset to state --- osu.Game/Online/Spectator/SpectatorState.cs | 13 ++++--------- .../Online/Spectator/SpectatorStreamingClient.cs | 5 +++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorState.cs b/osu.Game/Online/Spectator/SpectatorState.cs index 48fad4b3b2..101ce3d5d5 100644 --- a/osu.Game/Online/Spectator/SpectatorState.cs +++ b/osu.Game/Online/Spectator/SpectatorState.cs @@ -14,18 +14,13 @@ namespace osu.Game.Online.Spectator { public int? BeatmapID { get; set; } + public int? RulesetID { get; set; } + [NotNull] public IEnumerable Mods { get; set; } = Enumerable.Empty(); - public SpectatorState(int? beatmapId = null, IEnumerable mods = null) - { - BeatmapID = beatmapId; - if (mods != null) - Mods = mods; - } + public bool Equals(SpectatorState other) => BeatmapID == other?.BeatmapID && Mods.SequenceEqual(other?.Mods) && RulesetID == other?.RulesetID; - public bool Equals(SpectatorState other) => BeatmapID == other?.BeatmapID && Mods.SequenceEqual(other?.Mods); - - public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)}"; + public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)} Ruleset:{RulesetID}"; } } diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 1ca0a378bb..43bc8ff71b 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -15,6 +15,7 @@ using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Replays.Legacy; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; @@ -41,6 +42,9 @@ namespace osu.Game.Online.Spectator [Resolved] private IBindable beatmap { get; set; } + [Resolved] + private IBindable ruleset { get; set; } + [Resolved] private IBindable> mods { get; set; } @@ -171,6 +175,7 @@ namespace osu.Game.Online.Spectator // transfer state at point of beginning play currentState.BeatmapID = beatmap.Value.BeatmapInfo.OnlineBeatmapID; + currentState.RulesetID = ruleset.Value.ID; currentState.Mods = mods.Value.Select(m => new APIMod(m)); beginPlaying(); From ae9e60560bc3e00efb4b56692afd387d2a6e1564 Mon Sep 17 00:00:00 2001 From: Shivam Date: Fri, 23 Oct 2020 14:11:29 +0200 Subject: [PATCH 30/47] Fixed gameplay flags being bigger and changed values to make more sense --- osu.Game.Tournament/Components/DrawableTeamFlag.cs | 2 +- osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs | 1 + osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs | 2 +- osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tournament/Components/DrawableTeamFlag.cs b/osu.Game.Tournament/Components/DrawableTeamFlag.cs index a2e0bf83be..75991a1ab8 100644 --- a/osu.Game.Tournament/Components/DrawableTeamFlag.cs +++ b/osu.Game.Tournament/Components/DrawableTeamFlag.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tournament.Components { if (team == null) return; - Size = new Vector2(70, 47); + Size = new Vector2(75, 50); Masking = true; CornerRadius = 5; Child = flagSprite = new Sprite diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs index 44921f06ad..4ba86dcefc 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs @@ -29,6 +29,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components var anchor = flip ? Anchor.TopLeft : Anchor.TopRight; Flag.RelativeSizeAxes = Axes.None; + Flag.Scale = new Vector2(0.8f); Flag.Origin = anchor; Flag.Anchor = anchor; diff --git a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs index 32830713f6..55fc80dba2 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs @@ -288,7 +288,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro AutoSizeAxes = Axes.Both; Flag.RelativeSizeAxes = Axes.None; - Flag.Scale = new Vector2(1.4f); + Flag.Scale = new Vector2(1.2f); InternalChild = new FillFlowContainer { diff --git a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs index 3972c590ea..7ca262a2e8 100644 --- a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs +++ b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs @@ -93,7 +93,7 @@ namespace osu.Game.Tournament.Screens.TeamWin Anchor = Anchor.Centre, Origin = Anchor.Centre, Position = new Vector2(-300, 10), - Scale = new Vector2(2.2f) + Scale = new Vector2(2f) }, new FillFlowContainer { From c24a29d1acdde0228ffdd24f7726e0a0100f9e16 Mon Sep 17 00:00:00 2001 From: Shivam Date: Fri, 23 Oct 2020 14:23:08 +0200 Subject: [PATCH 31/47] Update flag scale of drawablematchteam --- .../Screens/Ladder/Components/DrawableMatchTeam.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs index 030ccb5cb3..ba577888d8 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs @@ -63,7 +63,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components this.losers = losers; Size = new Vector2(150, 40); - Flag.Scale = new Vector2(0.6f); + Flag.Scale = new Vector2(0.55f); Flag.Anchor = Flag.Origin = Anchor.CentreLeft; AcronymText.Anchor = AcronymText.Origin = Anchor.CentreLeft; From 73174961f02f01123f1c3cef900bf8a9475d8e90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Oct 2020 21:22:47 +0200 Subject: [PATCH 32/47] Rework animation sequence for readability --- osu.Game/Screens/Play/PlayerLoader.cs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index fae0bfb295..be3bad1517 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -331,18 +331,11 @@ namespace osu.Game.Screens.Play { const double epilepsy_display_length = 3000; - pushSequence.Schedule(() => - { - epilepsyWarning.State.Value = Visibility.Visible; - - this.Delay(epilepsy_display_length).Schedule(() => - { - epilepsyWarning.Hide(); - epilepsyWarning.Expire(); - }); - }); - - pushSequence.Delay(epilepsy_display_length); + pushSequence + .Schedule(() => epilepsyWarning.State.Value = Visibility.Visible) + .Delay(epilepsy_display_length) + .Schedule(() => epilepsyWarning.Hide()) + .Delay(EpilepsyWarning.FADE_DURATION); } pushSequence.Schedule(() => From e101ba5cba4eb63ef287a60d7d1bd121893f741d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 24 Oct 2020 22:58:13 +0200 Subject: [PATCH 33/47] Move volume manipulations to player loader --- osu.Game/Screens/Play/EpilepsyWarning.cs | 18 ------------------ osu.Game/Screens/Play/PlayerLoader.cs | 23 +++++++++++++++++++++-- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/Play/EpilepsyWarning.cs b/osu.Game/Screens/Play/EpilepsyWarning.cs index e3cf0cd227..6121a0c2a3 100644 --- a/osu.Game/Screens/Play/EpilepsyWarning.cs +++ b/osu.Game/Screens/Play/EpilepsyWarning.cs @@ -2,8 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -18,11 +16,6 @@ namespace osu.Game.Screens.Play { public class EpilepsyWarning : VisibilityContainer { - public const double FADE_DURATION = 500; - - private readonly BindableDouble trackVolumeOnEpilepsyWarning = new BindableDouble(1f); - - private Track track; public EpilepsyWarning() { @@ -77,26 +70,15 @@ namespace osu.Game.Screens.Play } } }; - - track = beatmap.Value.Track; - track.AddAdjustment(AdjustableProperty.Volume, trackVolumeOnEpilepsyWarning); } protected override void PopIn() { - this.TransformBindableTo(trackVolumeOnEpilepsyWarning, 0.25, FADE_DURATION); - DimmableBackground?.FadeColour(OsuColour.Gray(0.5f), FADE_DURATION, Easing.OutQuint); this.FadeIn(FADE_DURATION, Easing.OutQuint); } protected override void PopOut() => this.FadeOut(FADE_DURATION); - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - track?.RemoveAdjustment(AdjustableProperty.Volume, trackVolumeOnEpilepsyWarning); - } } } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index be3bad1517..fe774527b8 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -104,6 +104,9 @@ namespace osu.Game.Screens.Play [Resolved] private AudioManager audioManager { get; set; } + [Resolved] + private MusicController musicController { get; set; } + public PlayerLoader(Func createPlayer) { this.createPlayer = createPlayer; @@ -332,9 +335,17 @@ namespace osu.Game.Screens.Play const double epilepsy_display_length = 3000; pushSequence - .Schedule(() => epilepsyWarning.State.Value = Visibility.Visible) + .Schedule(() => + { + musicController.CurrentTrack.VolumeTo(0.25, EpilepsyWarning.FADE_DURATION, Easing.OutQuint); + epilepsyWarning.State.Value = Visibility.Visible; + }) .Delay(epilepsy_display_length) - .Schedule(() => epilepsyWarning.Hide()) + .Schedule(() => + { + epilepsyWarning.Hide(); + epilepsyWarning.Expire(); + }) .Delay(EpilepsyWarning.FADE_DURATION); } @@ -348,6 +359,10 @@ namespace osu.Game.Screens.Play // Note that this may change if the player we load requested a re-run. ValidForResume = false; + // restore full volume immediately - there's a usually a period of silence at start of gameplay anyway. + // note that this is delayed slightly to avoid volume spikes just before push. + musicController.CurrentTrack.Delay(50).VolumeTo(1); + if (player.LoadedBeatmapSuccessfully) this.Push(player); else @@ -363,6 +378,10 @@ namespace osu.Game.Screens.Play private void cancelLoad() { + // in case the epilepsy warning is being displayed, restore full volume. + if (epilepsyWarning?.IsAlive == true) + musicController.CurrentTrack.VolumeTo(1, EpilepsyWarning.FADE_DURATION, Easing.OutQuint); + scheduledPushPlayer?.Cancel(); scheduledPushPlayer = null; } From 85e14f3f0c8b3e194cb51057e4e9b2970dda7a21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 24 Oct 2020 22:58:43 +0200 Subject: [PATCH 34/47] Shorten fade duration to make fade out snappier --- osu.Game/Screens/Play/EpilepsyWarning.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Play/EpilepsyWarning.cs b/osu.Game/Screens/Play/EpilepsyWarning.cs index 6121a0c2a3..dc42427fbf 100644 --- a/osu.Game/Screens/Play/EpilepsyWarning.cs +++ b/osu.Game/Screens/Play/EpilepsyWarning.cs @@ -16,6 +16,7 @@ namespace osu.Game.Screens.Play { public class EpilepsyWarning : VisibilityContainer { + public const double FADE_DURATION = 250; public EpilepsyWarning() { From 8b04cd2cb0a79a81432bff9f488d12300240ebac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 25 Oct 2020 20:28:24 +0900 Subject: [PATCH 35/47] Fix a potential null reference when loading carousel difficulties --- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 703b91c517..93f95e76cc 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -140,7 +140,7 @@ namespace osu.Game.Screens.Select.Carousel LoadComponentAsync(beatmapContainer, loaded => { // make sure the pooled target hasn't changed. - if (carouselBeatmapSet != Item) + if (beatmapContainer != loaded) return; Content.Child = loaded; From 0542a45c43a14a16eec8a99570c070e642104fd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 25 Oct 2020 12:33:35 +0100 Subject: [PATCH 36/47] Change to manual adjustment add/remove --- osu.Game/Screens/Play/PlayerLoader.cs | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index fe774527b8..42074ac241 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -55,6 +55,8 @@ namespace osu.Game.Screens.Play private bool backgroundBrightnessReduction; + private readonly BindableDouble volumeAdjustment = new BindableDouble(1); + protected bool BackgroundBrightnessReduction { set @@ -104,9 +106,6 @@ namespace osu.Game.Screens.Play [Resolved] private AudioManager audioManager { get; set; } - [Resolved] - private MusicController musicController { get; set; } - public PlayerLoader(Func createPlayer) { this.createPlayer = createPlayer; @@ -172,6 +171,7 @@ namespace osu.Game.Screens.Play if (epilepsyWarning != null) epilepsyWarning.DimmableBackground = Background; + Beatmap.Value.Track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment); content.ScaleTo(0.7f); Background?.FadeColour(Color4.White, 800, Easing.OutQuint); @@ -200,6 +200,11 @@ namespace osu.Game.Screens.Play cancelLoad(); BackgroundBrightnessReduction = false; + + // we're moving to player, so a period of silence is upcoming. + // stop the track before removing adjustment to avoid a volume spike. + Beatmap.Value.Track.Stop(); + Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment); } public override bool OnExiting(IScreen next) @@ -211,6 +216,7 @@ namespace osu.Game.Screens.Play Background.EnableUserDim.Value = false; BackgroundBrightnessReduction = false; + Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment); return base.OnExiting(next); } @@ -335,11 +341,8 @@ namespace osu.Game.Screens.Play const double epilepsy_display_length = 3000; pushSequence - .Schedule(() => - { - musicController.CurrentTrack.VolumeTo(0.25, EpilepsyWarning.FADE_DURATION, Easing.OutQuint); - epilepsyWarning.State.Value = Visibility.Visible; - }) + .Schedule(() => epilepsyWarning.State.Value = Visibility.Visible) + .TransformBindableTo(volumeAdjustment, 0.25, EpilepsyWarning.FADE_DURATION, Easing.OutQuint) .Delay(epilepsy_display_length) .Schedule(() => { @@ -359,10 +362,6 @@ namespace osu.Game.Screens.Play // Note that this may change if the player we load requested a re-run. ValidForResume = false; - // restore full volume immediately - there's a usually a period of silence at start of gameplay anyway. - // note that this is delayed slightly to avoid volume spikes just before push. - musicController.CurrentTrack.Delay(50).VolumeTo(1); - if (player.LoadedBeatmapSuccessfully) this.Push(player); else @@ -378,10 +377,6 @@ namespace osu.Game.Screens.Play private void cancelLoad() { - // in case the epilepsy warning is being displayed, restore full volume. - if (epilepsyWarning?.IsAlive == true) - musicController.CurrentTrack.VolumeTo(1, EpilepsyWarning.FADE_DURATION, Easing.OutQuint); - scheduledPushPlayer?.Cancel(); scheduledPushPlayer = null; } From 0a23e994e2de7a23927928e4a051d6372a7c8dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 25 Oct 2020 23:24:14 +0100 Subject: [PATCH 37/47] Hide sliderend & repeat circles in traceable mod --- osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index d7582f3196..e1d197fb1d 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Bindables; using System.Collections.Generic; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; @@ -49,9 +50,16 @@ namespace osu.Game.Rulesets.Osu.Mods { case DrawableHitCircle circle: // we only want to see the approach circle - using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true)) - circle.CirclePiece.Hide(); + applyCirclePieceState(circle, circle.CirclePiece); + break; + case DrawableSliderTail sliderTail: + applyCirclePieceState(sliderTail); + break; + + case DrawableSliderRepeat sliderRepeat: + // show only the repeat arrow + applyCirclePieceState(sliderRepeat, sliderRepeat.CirclePiece); break; case DrawableSlider slider: @@ -61,6 +69,13 @@ namespace osu.Game.Rulesets.Osu.Mods } } + private void applyCirclePieceState(DrawableOsuHitObject hitObject, IDrawable hitCircle = null) + { + var h = hitObject.HitObject; + using (hitObject.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true)) + (hitCircle ?? hitObject).Hide(); + } + private void applySliderState(DrawableSlider slider) { ((PlaySliderBody)slider.Body.Drawable).AccentColour = slider.AccentColour.Value.Opacity(0); From 5ef1b5dcb521a7d75f4f8dd6e7e82c934a3c195c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 25 Oct 2020 23:55:22 +0100 Subject: [PATCH 38/47] Remove unused locals --- osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index e1d197fb1d..bb2213aa31 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -39,11 +39,9 @@ namespace osu.Game.Rulesets.Osu.Mods protected void ApplyTraceableState(DrawableHitObject drawable, ArmedState state) { - if (!(drawable is DrawableOsuHitObject drawableOsu)) + if (!(drawable is DrawableOsuHitObject)) return; - var h = drawableOsu.HitObject; - //todo: expose and hide spinner background somehow switch (drawable) From 9caa7ff64dc12e6cd6f536ef7cab9a57fa339a55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Oct 2020 13:37:16 +0900 Subject: [PATCH 39/47] Remove debug endpoint --- osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 43bc8ff71b..97901184c7 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -80,11 +80,7 @@ namespace osu.Game.Online.Spectator } } -#if DEBUG - private const string endpoint = "http://localhost:5009/spectator"; -#else private const string endpoint = "https://spectator.ppy.sh/spectator"; -#endif private async Task connect() { From ac13a1d21708b7f54a53d818a7e5ac16f0c35936 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Oct 2020 14:27:55 +0900 Subject: [PATCH 40/47] Adjust a couple of flag scales to match previous display size --- osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs | 2 +- .../Screens/Ladder/Components/DrawableMatchTeam.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs b/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs index 119f71ebfa..cd252392ba 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components AcronymText.Origin = Anchor.TopCentre; AcronymText.Text = team.Acronym.Value.ToUpperInvariant(); AcronymText.Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 10); - Flag.Scale = new Vector2(0.5f); + Flag.Scale = new Vector2(0.48f); InternalChildren = new Drawable[] { diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs index ba577888d8..bb1e4d2eff 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs @@ -63,7 +63,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components this.losers = losers; Size = new Vector2(150, 40); - Flag.Scale = new Vector2(0.55f); + Flag.Scale = new Vector2(0.54f); Flag.Anchor = Flag.Origin = Anchor.CentreLeft; AcronymText.Anchor = AcronymText.Origin = Anchor.CentreLeft; From e941f2fb711d5d57308203c788a9d594e83bae0d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Oct 2020 15:24:12 +0900 Subject: [PATCH 41/47] Fix playback not being smooth (and event unbinding logic) --- .../Visual/Gameplay/TestSceneSpectator.cs | 53 +++++++++++++++---- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index f8b5d385a9..4db9d955d4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -12,7 +13,9 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Input.StateChanges; +using osu.Framework.Logging; using osu.Framework.Testing; +using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Online.Spectator; @@ -41,6 +44,10 @@ namespace osu.Game.Tests.Visual.Gameplay private TestReplayRecorder recorder; + private readonly ManualClock manualClock = new ManualClock(); + + private OsuSpriteText latencyDisplay; + [Resolved] private SpectatorStreamingClient streamingClient { get; set; } @@ -66,15 +73,7 @@ namespace osu.Game.Tests.Visual.Gameplay } }, true); - streamingClient.OnNewFrames += (userId, frames) => - { - foreach (var legacyFrame in frames.Frames) - { - var frame = new TestReplayFrame(); - frame.FromLegacy(legacyFrame, null, null); - replay.Frames.Add(frame); - } - }; + streamingClient.OnNewFrames += onNewFrames; Add(new GridContainer { @@ -115,6 +114,7 @@ namespace osu.Game.Tests.Visual.Gameplay { playbackManager = new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique) { + Clock = new FramedClock(manualClock), ReplayInputHandler = new TestFramedReplayInputHandler(replay) { GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos), @@ -143,8 +143,22 @@ namespace osu.Game.Tests.Visual.Gameplay } } }); + + Add(latencyDisplay = new OsuSpriteText()); }); + private void onNewFrames(int userId, FrameDataBundle frames) + { + Logger.Log($"Received {frames.Frames.Count()} new frames ({string.Join(',', frames.Frames.Select(f => ((int)f.Time).ToString()))})"); + + foreach (var legacyFrame in frames.Frames) + { + var frame = new TestReplayFrame(); + frame.FromLegacy(legacyFrame, null, null); + replay.Frames.Add(frame); + } + } + [Test] public void TestBasic() { @@ -153,13 +167,30 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void Update() { base.Update(); - playbackManager?.ReplayInputHandler.SetFrameFromTime(Time.Current - 100); + + double elapsed = Time.Elapsed; + double? time = playbackManager?.ReplayInputHandler.SetFrameFromTime(manualClock.CurrentTime + elapsed); + + if (time != null) + { + manualClock.CurrentTime = time.Value; + + latencyDisplay.Text = $"latency: {Time.Current - time.Value:N1}ms"; + } + else + { + manualClock.CurrentTime = Time.Current; + } } [TearDownSteps] public void TearDown() { - AddStep("stop recorder", () => recorder.Expire()); + AddStep("stop recorder", () => + { + recorder.Expire(); + streamingClient.OnNewFrames -= onNewFrames; + }); } public class TestFramedReplayInputHandler : FramedReplayInputHandler From 8508d5f8b94d04a373f49b087d739ee4c93bbf62 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Oct 2020 15:24:28 +0900 Subject: [PATCH 42/47] Rename test scene to match purpose --- .../{TestSceneSpectator.cs => TestSceneSpectatorPlayback.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osu.Game.Tests/Visual/Gameplay/{TestSceneSpectator.cs => TestSceneSpectatorPlayback.cs} (99%) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs similarity index 99% rename from osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs rename to osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 4db9d955d4..2656b7929c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -31,7 +31,7 @@ using osuTK.Graphics; namespace osu.Game.Tests.Visual.Gameplay { - public class TestSceneSpectator : OsuManualInputManagerTestScene + public class TestSceneSpectatorPlayback : OsuManualInputManagerTestScene { protected override bool UseOnlineAPI => true; From f5dbaa9b0fab2bf2b4b805cec6d914897219ff3b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Oct 2020 15:25:09 +0900 Subject: [PATCH 43/47] Only watch local user to prevent conflict between testers --- .../Gameplay/TestSceneSpectatorPlayback.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 2656b7929c..e7b7950ad2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -18,6 +18,7 @@ using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; +using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Replays; using osu.Game.Replays.Legacy; @@ -48,6 +49,9 @@ namespace osu.Game.Tests.Visual.Gameplay private OsuSpriteText latencyDisplay; + [Resolved] + private IAPIProvider api { get; set; } + [Resolved] private SpectatorStreamingClient streamingClient { get; set; } @@ -63,12 +67,20 @@ namespace osu.Game.Tests.Visual.Gameplay { case NotifyCollectionChangedAction.Add: foreach (int user in args.NewItems) - streamingClient.WatchUser(user); + { + if (user == api.LocalUser.Value.Id) + streamingClient.WatchUser(user); + } + break; case NotifyCollectionChangedAction.Remove: foreach (int user in args.OldItems) - streamingClient.StopWatchingUser(user); + { + if (user == api.LocalUser.Value.Id) + streamingClient.StopWatchingUser(user); + } + break; } }, true); From dfe07271de741378553f6fc872f0072e5a050979 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Oct 2020 16:31:39 +0900 Subject: [PATCH 44/47] Add very basic latency handling to spectator test --- .../Gameplay/TestSceneSpectatorPlayback.cs | 43 ++++++++++++++----- .../Spectator/SpectatorStreamingClient.cs | 9 ++-- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index e7b7950ad2..d27a41acd4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -1,6 +1,7 @@ // 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 System.Collections.Generic; using System.Collections.Specialized; using System.Linq; @@ -18,6 +19,7 @@ using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; +using osu.Game.Input.Handlers; using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Replays; @@ -49,6 +51,8 @@ namespace osu.Game.Tests.Visual.Gameplay private OsuSpriteText latencyDisplay; + private TestFramedReplayInputHandler replayHandler; + [Resolved] private IAPIProvider api { get; set; } @@ -127,7 +131,7 @@ namespace osu.Game.Tests.Visual.Gameplay playbackManager = new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique) { Clock = new FramedClock(manualClock), - ReplayInputHandler = new TestFramedReplayInputHandler(replay) + ReplayInputHandler = replayHandler = new TestFramedReplayInputHandler(replay) { GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos), }, @@ -176,22 +180,41 @@ namespace osu.Game.Tests.Visual.Gameplay { } + private double latency = SpectatorStreamingClient.TIME_BETWEEN_SENDS; + protected override void Update() { base.Update(); - double elapsed = Time.Elapsed; - double? time = playbackManager?.ReplayInputHandler.SetFrameFromTime(manualClock.CurrentTime + elapsed); + if (latencyDisplay == null) return; - if (time != null) - { - manualClock.CurrentTime = time.Value; - - latencyDisplay.Text = $"latency: {Time.Current - time.Value:N1}ms"; - } - else + // propagate initial time value + if (manualClock.CurrentTime == 0) { manualClock.CurrentTime = Time.Current; + return; + } + + if (replayHandler.NextFrame != null) + { + var lastFrame = replay.Frames.LastOrDefault(); + + // this isn't perfect as we basically can't be aware of the rate-of-send here (the streamer is not sending data when not being moved). + // in gameplay playback, the case where NextFrame is null would pause gameplay and handle this correctly; it's strictly a test limitation / best effort implementation. + if (lastFrame != null) + latency = Math.Max(latency, Time.Current - lastFrame.Time); + + latencyDisplay.Text = $"latency: {latency:N1}"; + + double proposedTime = Time.Current - latency + Time.Elapsed; + + // this will either advance by one or zero frames. + double? time = replayHandler.SetFrameFromTime(proposedTime); + + if (time == null) + return; + + manualClock.CurrentTime = time.Value; } } diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 97901184c7..73a18b03b2 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -24,6 +24,11 @@ namespace osu.Game.Online.Spectator { public class SpectatorStreamingClient : Component, ISpectatorClient { + /// + /// The maximum milliseconds between frame bundle sends. + /// + public const double TIME_BETWEEN_SENDS = 200; + private HubConnection connection; private readonly List watchingUsers = new List(); @@ -229,15 +234,13 @@ namespace osu.Game.Online.Spectator private Task lastSend; - private const double time_between_sends = 200; - private const int max_pending_frames = 30; protected override void Update() { base.Update(); - if (pendingFrames.Count > 0 && Time.Current - lastSendTime > time_between_sends) + if (pendingFrames.Count > 0 && Time.Current - lastSendTime > TIME_BETWEEN_SENDS) purgePendingFrames(); } From b1a88a49935c1c497113e5f4de843b72199130a4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Oct 2020 16:34:30 +0900 Subject: [PATCH 45/47] Remove extra using --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index d27a41acd4..ad11ac45dd 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -19,7 +19,6 @@ using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; -using osu.Game.Input.Handlers; using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Replays; From 7392876b5f71a5bfb4b22fcabb0a0e38cef5a368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 27 Oct 2020 00:05:03 +0100 Subject: [PATCH 46/47] Fix mania crashing due to spectator client handling frames with unconverted beatmap --- .../Visual/Gameplay/TestSceneReplayRecorder.cs | 6 ++++++ .../Visual/Gameplay/TestSceneReplayRecording.cs | 6 ++++++ .../Visual/Gameplay/TestSceneSpectatorPlayback.cs | 4 ++++ .../Online/Spectator/SpectatorStreamingClient.cs | 14 +++++++++----- osu.Game/Rulesets/UI/ReplayRecorder.cs | 7 +++---- 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index e964d2a40e..47dd47959d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -12,11 +13,13 @@ using osu.Framework.Input.Events; using osu.Framework.Input.StateChanges; using osu.Framework.Testing; using osu.Framework.Threading; +using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; using osu.Game.Tests.Visual.UserInterface; using osuTK; using osuTK.Graphics; @@ -33,6 +36,9 @@ namespace osu.Game.Tests.Visual.Gameplay private TestReplayRecorder recorder; + [Cached] + private GameplayBeatmap gameplayBeatmap = new GameplayBeatmap(new Beatmap()); + [SetUp] public void SetUp() => Schedule(() => { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs index c0f99db85d..6872b6a669 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs @@ -2,17 +2,20 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Input.StateChanges; +using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; using osu.Game.Tests.Visual.UserInterface; using osuTK; using osuTK.Graphics; @@ -25,6 +28,9 @@ namespace osu.Game.Tests.Visual.Gameplay private readonly TestRulesetInputManager recordingManager; + [Cached] + private GameplayBeatmap gameplayBeatmap = new GameplayBeatmap(new Beatmap()); + public TestSceneReplayRecording() { Replay replay = new Replay(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index ad11ac45dd..1d8231cce7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -27,6 +27,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; using osu.Game.Tests.Visual.UserInterface; using osuTK; using osuTK.Graphics; @@ -58,6 +59,9 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private SpectatorStreamingClient streamingClient { get; set; } + [Cached] + private GameplayBeatmap gameplayBeatmap = new GameplayBeatmap(new Beatmap()); + [SetUp] public void SetUp() => Schedule(() => { diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 73a18b03b2..7059818b4e 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using JetBrains.Annotations; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; @@ -19,6 +20,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; +using osu.Game.Screens.Play; namespace osu.Game.Online.Spectator { @@ -44,8 +46,8 @@ namespace osu.Game.Online.Spectator [Resolved] private IAPIProvider api { get; set; } - [Resolved] - private IBindable beatmap { get; set; } + [CanBeNull] + private IBeatmap currentBeatmap; [Resolved] private IBindable ruleset { get; set; } @@ -167,7 +169,7 @@ namespace osu.Game.Online.Spectator return Task.CompletedTask; } - public void BeginPlaying() + public void BeginPlaying(GameplayBeatmap beatmap) { if (isPlaying) throw new InvalidOperationException($"Cannot invoke {nameof(BeginPlaying)} when already playing"); @@ -175,10 +177,11 @@ namespace osu.Game.Online.Spectator isPlaying = true; // transfer state at point of beginning play - currentState.BeatmapID = beatmap.Value.BeatmapInfo.OnlineBeatmapID; + currentState.BeatmapID = beatmap.BeatmapInfo.OnlineBeatmapID; currentState.RulesetID = ruleset.Value.ID; currentState.Mods = mods.Value.Select(m => new APIMod(m)); + currentBeatmap = beatmap.PlayableBeatmap; beginPlaying(); } @@ -201,6 +204,7 @@ namespace osu.Game.Online.Spectator public void EndPlaying() { isPlaying = false; + currentBeatmap = null; if (!isConnected) return; @@ -247,7 +251,7 @@ namespace osu.Game.Online.Spectator public void HandleFrame(ReplayFrame frame) { if (frame is IConvertibleReplayFrame convertible) - pendingFrames.Enqueue(convertible.ToLegacy(beatmap.Value.Beatmap)); + pendingFrames.Enqueue(convertible.ToLegacy(currentBeatmap)); if (pendingFrames.Count > max_pending_frames) purgePendingFrames(); diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index a84b4f4ba8..1438ebd37a 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -5,15 +5,14 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Game.Beatmaps; using osu.Game.Online.Spectator; using osu.Game.Replays; using osu.Game.Rulesets.Replays; +using osu.Game.Screens.Play; using osuTK; namespace osu.Game.Rulesets.UI @@ -33,7 +32,7 @@ namespace osu.Game.Rulesets.UI private SpectatorStreamingClient spectatorStreaming { get; set; } [Resolved] - private IBindable beatmap { get; set; } + private GameplayBeatmap gameplayBeatmap { get; set; } protected ReplayRecorder(Replay target) { @@ -50,7 +49,7 @@ namespace osu.Game.Rulesets.UI inputManager = GetContainingInputManager(); - spectatorStreaming?.BeginPlaying(); + spectatorStreaming?.BeginPlaying(gameplayBeatmap); } protected override void Dispose(bool isDisposing) From 68719bb23df43e7d18cd429042f05f240e432495 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Oct 2020 10:59:24 +0900 Subject: [PATCH 47/47] Rename other variables to match --- osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 7059818b4e..5a41316f31 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -50,10 +50,10 @@ namespace osu.Game.Online.Spectator private IBeatmap currentBeatmap; [Resolved] - private IBindable ruleset { get; set; } + private IBindable currentRuleset { get; set; } [Resolved] - private IBindable> mods { get; set; } + private IBindable> currentMods { get; set; } private readonly SpectatorState currentState = new SpectatorState(); @@ -178,8 +178,8 @@ namespace osu.Game.Online.Spectator // transfer state at point of beginning play currentState.BeatmapID = beatmap.BeatmapInfo.OnlineBeatmapID; - currentState.RulesetID = ruleset.Value.ID; - currentState.Mods = mods.Value.Select(m => new APIMod(m)); + currentState.RulesetID = currentRuleset.Value.ID; + currentState.Mods = currentMods.Value.Select(m => new APIMod(m)); currentBeatmap = beatmap.PlayableBeatmap; beginPlaying();