diff --git a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs index db14dc95b2..5761a89ae8 100644 --- a/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs +++ b/osu.Game.Tests/OnlinePlay/TestSceneCatchUpSyncManager.cs @@ -19,20 +19,20 @@ namespace osu.Game.Tests.OnlinePlay public class TestSceneCatchUpSyncManager : OsuTestScene { private GameplayClockContainer master; - private CatchUpSyncManager syncManager; + private SpectatorSyncManager syncManager; - private Dictionary clocksById; - private ISpectatorPlayerClock player1; - private ISpectatorPlayerClock player2; + private Dictionary clocksById; + private SpectatorPlayerClock player1; + private SpectatorPlayerClock player2; [SetUp] public void Setup() { - syncManager = new CatchUpSyncManager(master = new GameplayClockContainer(new TestManualClock())); + syncManager = new SpectatorSyncManager(master = new GameplayClockContainer(new TestManualClock())); player1 = syncManager.CreateManagedClock(); player2 = syncManager.CreateManagedClock(); - clocksById = new Dictionary + clocksById = new Dictionary { { player1, 1 }, { player2, 2 } @@ -64,7 +64,7 @@ namespace osu.Game.Tests.OnlinePlay public void TestReadyPlayersStartWhenReadyForMaximumDelayTime() { setWaiting(() => player1, false); - AddWaitStep($"wait {CatchUpSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction)); + AddWaitStep($"wait {SpectatorSyncManager.MAXIMUM_START_DELAY} milliseconds", (int)Math.Ceiling(SpectatorSyncManager.MAXIMUM_START_DELAY / TimePerAction)); assertPlayerClockState(() => player1, true); assertPlayerClockState(() => player2, false); } @@ -74,7 +74,7 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setMasterTime(CatchUpSyncManager.SYNC_TARGET + 1); + setMasterTime(SpectatorSyncManager.SYNC_TARGET + 1); assertCatchingUp(() => player1, false); } @@ -83,7 +83,7 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setMasterTime(CatchUpSyncManager.MAX_SYNC_OFFSET + 1); + setMasterTime(SpectatorSyncManager.MAX_SYNC_OFFSET + 1); assertCatchingUp(() => player1, true); assertCatchingUp(() => player2, true); } @@ -93,8 +93,8 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setMasterTime(CatchUpSyncManager.MAX_SYNC_OFFSET + 1); - setPlayerClockTime(() => player1, CatchUpSyncManager.SYNC_TARGET + 1); + setMasterTime(SpectatorSyncManager.MAX_SYNC_OFFSET + 1); + setPlayerClockTime(() => player1, SpectatorSyncManager.SYNC_TARGET + 1); assertCatchingUp(() => player1, true); } @@ -103,8 +103,8 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setMasterTime(CatchUpSyncManager.MAX_SYNC_OFFSET + 2); - setPlayerClockTime(() => player1, CatchUpSyncManager.SYNC_TARGET); + setMasterTime(SpectatorSyncManager.MAX_SYNC_OFFSET + 2); + setPlayerClockTime(() => player1, SpectatorSyncManager.SYNC_TARGET); assertCatchingUp(() => player1, false); assertCatchingUp(() => player2, true); } @@ -114,7 +114,7 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setPlayerClockTime(() => player1, -CatchUpSyncManager.SYNC_TARGET); + setPlayerClockTime(() => player1, -SpectatorSyncManager.SYNC_TARGET); assertCatchingUp(() => player1, false); assertPlayerClockState(() => player1, true); } @@ -124,7 +124,7 @@ namespace osu.Game.Tests.OnlinePlay { setAllWaiting(false); - setPlayerClockTime(() => player1, -CatchUpSyncManager.SYNC_TARGET - 1); + setPlayerClockTime(() => player1, -SpectatorSyncManager.SYNC_TARGET - 1); // This is a silent catchup, where IsCatchingUp = false but IsRunning = false also. assertCatchingUp(() => player1, false); @@ -145,7 +145,7 @@ namespace osu.Game.Tests.OnlinePlay assertPlayerClockState(() => player1, false); } - private void setWaiting(Func playerClock, bool waiting) + private void setWaiting(Func playerClock, bool waiting) => AddStep($"set player clock {clocksById[playerClock()]} waiting = {waiting}", () => playerClock().WaitingOnFrames.Value = waiting); private void setAllWaiting(bool waiting) => AddStep($"set all player clocks waiting = {waiting}", () => @@ -160,13 +160,13 @@ namespace osu.Game.Tests.OnlinePlay /// /// clock.Time = master.Time - offsetFromMaster /// - private void setPlayerClockTime(Func playerClock, double offsetFromMaster) + private void setPlayerClockTime(Func playerClock, double offsetFromMaster) => AddStep($"set player clock {clocksById[playerClock()]} = master - {offsetFromMaster}", () => playerClock().Seek(master.CurrentTime - offsetFromMaster)); - private void assertCatchingUp(Func playerClock, bool catchingUp) => + private void assertCatchingUp(Func playerClock, bool catchingUp) => AddAssert($"player clock {clocksById[playerClock()]} {(catchingUp ? "is" : "is not")} catching up", () => playerClock().IsCatchingUp == catchingUp); - private void assertPlayerClockState(Func playerClock, bool running) + private void assertPlayerClockState(Func playerClock, bool running) => AddAssert($"player clock {clocksById[playerClock()]} {(running ? "is" : "is not")} running", () => playerClock().IsRunning == running); private class TestManualClock : ManualClock, IAdjustableClock diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index a2e3ab7318..bab613bed7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -202,7 +202,7 @@ namespace osu.Game.Tests.Visual.Multiplayer checkPausedInstant(PLAYER_2_ID, true); // Wait for the start delay seconds... - AddWaitStep("wait maximum start delay seconds", (int)(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction)); + AddWaitStep("wait maximum start delay seconds", (int)(SpectatorSyncManager.MAXIMUM_START_DELAY / TimePerAction)); // Player 1 should start playing by itself, player 2 should remain paused. checkPausedInstant(PLAYER_1_ID, false); @@ -318,7 +318,7 @@ namespace osu.Game.Tests.Visual.Multiplayer loadSpectateScreen(); sendFrames(PLAYER_1_ID, 300); - AddWaitStep("wait maximum start delay seconds", (int)(CatchUpSyncManager.MAXIMUM_START_DELAY / TimePerAction)); + AddWaitStep("wait maximum start delay seconds", (int)(SpectatorSyncManager.MAXIMUM_START_DELAY / TimePerAction)); checkPaused(PLAYER_1_ID, false); sendFrames(PLAYER_2_ID, 300); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISpectatorPlayerClock.cs deleted file mode 100644 index a2e6df9282..0000000000 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISpectatorPlayerClock.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Bindables; -using osu.Framework.Timing; - -namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate -{ - /// - /// A clock which is used by s and managed by an . - /// - public interface ISpectatorPlayerClock : IFrameBasedClock, IAdjustableClock - { - /// - /// Starts this . - /// - new void Start(); - - /// - /// Stops this . - /// - new void Stop(); - - /// - /// Whether this clock is waiting on frames to continue playback. - /// - Bindable WaitingOnFrames { get; } - - /// - /// Whether this clock is behind the master clock and running at a higher rate to catch up to it. - /// - /// - /// Of note, this will be false if this clock is *ahead* of the master clock. - /// - bool IsCatchingUp { get; set; } - } -} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs deleted file mode 100644 index 5615e02336..0000000000 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/ISyncManager.cs +++ /dev/null @@ -1,42 +0,0 @@ -// 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 osu.Framework.Bindables; -using osu.Game.Screens.Play; - -namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate -{ - /// - /// Manages the synchronisation between one or more s in relation to a master clock. - /// - public interface ISyncManager - { - /// - /// An event which is invoked when gameplay is ready to start. - /// - event Action? ReadyToStart; - - /// - /// The master clock which player clocks should synchronise to. - /// - GameplayClockContainer MasterClock { get; } - - /// - /// An event which is invoked when the state of is changed. - /// - IBindable MasterState { get; } - - /// - /// Create a new managed . - /// - /// The newly created . - ISpectatorPlayerClock CreateManagedClock(); - - /// - /// Removes an , stopping it from being managed by this . - /// - /// The to remove. - void RemoveManagedClock(ISpectatorPlayerClock clock); - } -} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs index 68eae76030..57af929cb7 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorPlayer.cs @@ -15,14 +15,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public class MultiSpectatorPlayer : SpectatorPlayer { private readonly Bindable waitingOnFrames = new Bindable(true); - private readonly ISpectatorPlayerClock spectatorPlayerClock; + private readonly SpectatorPlayerClock spectatorPlayerClock; /// /// Creates a new . /// /// The score containing the player's replay. /// The clock controlling the gameplay running state. - public MultiSpectatorPlayer(Score score, ISpectatorPlayerClock spectatorPlayerClock) + public MultiSpectatorPlayer(Score score, SpectatorPlayerClock spectatorPlayerClock) : base(score, new PlayerConfiguration { AllowUserInteraction = false }) { this.spectatorPlayerClock = spectatorPlayerClock; @@ -40,9 +40,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate protected override void Update() { // The player clock's running state is controlled externally, but the local pausing state needs to be updated to start/stop gameplay. - CatchUpSpectatorPlayerClock catchUpClock = (CatchUpSpectatorPlayerClock)GameplayClockContainer.SourceClock; - - if (catchUpClock.IsRunning) + if (GameplayClockContainer.SourceClock.IsRunning) GameplayClockContainer.Start(); else GameplayClockContainer.Stop(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 3d04ae8f3c..b285d3d7c2 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -8,6 +8,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Online.Multiplayer; @@ -47,7 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private readonly PlayerArea[] instances; private MasterGameplayClockContainer masterClockContainer = null!; - private ISyncManager syncManager = null!; + private SpectatorSyncManager syncManager = null!; private PlayerGrid grid = null!; private MultiSpectatorLeaderboard leaderboard = null!; private PlayerArea? currentAudioSource; @@ -80,7 +81,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate InternalChildren = new[] { - (Drawable)(syncManager = new CatchUpSyncManager(masterClockContainer)), + (Drawable)(syncManager = new SpectatorSyncManager(masterClockContainer)), masterClockContainer.WithChild(new GridContainer { RelativeSizeAxes = Axes.Both, @@ -168,7 +169,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate if (!isCandidateAudioSource(currentAudioSource?.GameplayClock)) { currentAudioSource = instances.Where(i => isCandidateAudioSource(i.GameplayClock)) - .OrderBy(i => Math.Abs(i.GameplayClock.CurrentTime - syncManager.MasterClock.CurrentTime)) + .OrderBy(i => Math.Abs(i.GameplayClock.CurrentTime - syncManager.CurrentMasterTime)) .FirstOrDefault(); foreach (var instance in instances) @@ -176,7 +177,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } } - private bool isCandidateAudioSource(ISpectatorPlayerClock? clock) + private bool isCandidateAudioSource(SpectatorPlayerClock? clock) => clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames.Value; private void onReadyToStart() @@ -198,6 +199,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private void onMasterStateChanged(ValueChangedEvent state) { + Logger.Log($"{nameof(MultiSpectatorScreen)}'s master clock become {state.NewValue}"); + switch (state.NewValue) { case MasterClockState.Synchronised: diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index 7e679383c4..a1fbdc10de 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -38,9 +38,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public readonly int UserId; /// - /// The used to control the gameplay running state of a loaded . + /// The used to control the gameplay running state of a loaded . /// - public readonly ISpectatorPlayerClock GameplayClock; + public readonly SpectatorPlayerClock GameplayClock; /// /// The currently-loaded score. @@ -55,7 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate private readonly LoadingLayer loadingLayer; private OsuScreenStack? stack; - public PlayerArea(int userId, ISpectatorPlayerClock clock) + public PlayerArea(int userId, SpectatorPlayerClock clock) { UserId = userId; GameplayClock = clock; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs similarity index 55% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs index 8d8f6a373a..4e785ad3b1 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSpectatorPlayerClock.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorPlayerClock.cs @@ -4,44 +4,58 @@ using System; using osu.Framework.Bindables; using osu.Framework.Timing; +using osu.Game.Screens.Play; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { /// - /// A which catches up using rate adjustment. + /// A clock which catches up using rate adjustment. /// - public class CatchUpSpectatorPlayerClock : ISpectatorPlayerClock + public class SpectatorPlayerClock : IFrameBasedClock, IAdjustableClock { /// /// The catch up rate. /// public const double CATCHUP_RATE = 2; - public readonly IFrameBasedClock Source; + private readonly GameplayClockContainer masterClock; public double CurrentTime { get; private set; } - public bool IsRunning { get; private set; } + /// + /// Whether this clock is waiting on frames to continue playback. + /// + public Bindable WaitingOnFrames { get; } = new Bindable(true); - public CatchUpSpectatorPlayerClock(IFrameBasedClock source) + /// + /// Whether this clock is behind the master clock and running at a higher rate to catch up to it. + /// + /// + /// Of note, this will be false if this clock is *ahead* of the master clock. + /// + public bool IsCatchingUp { get; set; } + + /// + /// Whether this spectator clock should be running. + /// Use instead of / to control time. + /// + public bool IsRunning { get; set; } + + public SpectatorPlayerClock(GameplayClockContainer masterClock) { - Source = source; + this.masterClock = masterClock; } public void Reset() => CurrentTime = 0; - public void Start() => IsRunning = true; - - public void Stop() => IsRunning = false; - - void IAdjustableClock.Start() + public void Start() { - // Our running state should only be managed by an ISyncManager, ignore calls from external sources. + // Our running state should only be managed by SpectatorSyncManager via IsRunning. } - void IAdjustableClock.Stop() + public void Stop() { - // Our running state should only be managed by an ISyncManager, ignore calls from external sources. + // Our running state should only be managed by an SpectatorSyncManager via IsRunning. } public bool Seek(double position) @@ -69,16 +83,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate ElapsedFrameTime = 0; FramesPerSecond = 0; - Source.ProcessFrame(); + masterClock.ProcessFrame(); if (IsRunning) { - double elapsedSource = Source.ElapsedFrameTime; + double elapsedSource = masterClock.ElapsedFrameTime; double elapsed = elapsedSource * Rate; CurrentTime += elapsed; ElapsedFrameTime = elapsed; - FramesPerSecond = Source.FramesPerSecond; + FramesPerSecond = masterClock.FramesPerSecond; } } @@ -87,9 +101,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate public double FramesPerSecond { get; private set; } public FrameTimeInfo TimeInfo => new FrameTimeInfo { Elapsed = ElapsedFrameTime, Current = CurrentTime }; - - public Bindable WaitingOnFrames { get; } = new Bindable(true); - - public bool IsCatchingUp { get; set; } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs similarity index 76% rename from osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs index 4e563ec69a..119e82a682 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/CatchUpSyncManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/SpectatorSyncManager.cs @@ -11,9 +11,9 @@ using osu.Game.Screens.Play; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { /// - /// A which synchronises de-synced player clocks through catchup. + /// Manages the synchronisation between one or more s in relation to a master clock. /// - public class CatchUpSyncManager : Component, ISyncManager + public class SpectatorSyncManager : Component { /// /// The offset from the master clock to which player clocks should remain within to be considered in-sync. @@ -30,41 +30,57 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public const double MAXIMUM_START_DELAY = 15000; + /// + /// An event which is invoked when gameplay is ready to start. + /// public event Action? ReadyToStart; + /// + /// The catch-up state of the master clock. + /// + public IBindable MasterState => masterState; + + public double CurrentMasterTime => masterClock.CurrentTime; + /// /// The master clock which is used to control the timing of all player clocks clocks. /// - public GameplayClockContainer MasterClock { get; } - - public IBindable MasterState => masterState; + private readonly GameplayClockContainer masterClock; /// /// The player clocks. /// - private readonly List playerClocks = new List(); + private readonly List playerClocks = new List(); private readonly Bindable masterState = new Bindable(); private bool hasStarted; private double? firstStartAttemptTime; - public CatchUpSyncManager(GameplayClockContainer master) + public SpectatorSyncManager(GameplayClockContainer master) { - MasterClock = master; + masterClock = master; } - public ISpectatorPlayerClock CreateManagedClock() + /// + /// Create a new managed . + /// + /// The newly created . + public SpectatorPlayerClock CreateManagedClock() { - var clock = new CatchUpSpectatorPlayerClock(MasterClock); + var clock = new SpectatorPlayerClock(masterClock); playerClocks.Add(clock); return clock; } - public void RemoveManagedClock(ISpectatorPlayerClock clock) + /// + /// Removes an , stopping it from being managed by this . + /// + /// The to remove. + public void RemoveManagedClock(SpectatorPlayerClock clock) { playerClocks.Remove(clock); - clock.Stop(); + clock.IsRunning = false; } protected override void Update() @@ -75,7 +91,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { // Ensure all player clocks are stopped until the start succeeds. foreach (var clock in playerClocks) - clock.Stop(); + clock.IsRunning = false; return; } @@ -128,7 +144,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate // How far this player's clock is out of sync, compared to the master clock. // A negative value means the player is running fast (ahead); a positive value means the player is running behind (catching up). - double timeDelta = MasterClock.CurrentTime - clock.CurrentTime; + double timeDelta = masterClock.CurrentTime - clock.CurrentTime; // Check that the player clock isn't too far ahead. // This is a quiet case in which the catchup is done by the master clock, so IsCatchingUp is not set on the player clock. @@ -137,15 +153,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate // Importantly, set the clock to a non-catchup state. if this isn't done, updateMasterState may incorrectly pause the master clock // when it is required to be running (ie. if all players are ahead of the master). clock.IsCatchingUp = false; - clock.Stop(); + clock.IsRunning = false; continue; } // Make sure the player clock is running if it can. - if (!clock.WaitingOnFrames.Value) - clock.Start(); - else - clock.Stop(); + clock.IsRunning = !clock.WaitingOnFrames.Value; if (clock.IsCatchingUp) {