1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-15 16:03:01 +08:00

Merge branch 'master' into add-date-created-sort

This commit is contained in:
Andrew Hong 2022-08-24 03:23:27 -04:00 committed by GitHub
commit a8867d4245
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 97 additions and 152 deletions

View File

@ -19,20 +19,20 @@ namespace osu.Game.Tests.OnlinePlay
public class TestSceneCatchUpSyncManager : OsuTestScene public class TestSceneCatchUpSyncManager : OsuTestScene
{ {
private GameplayClockContainer master; private GameplayClockContainer master;
private CatchUpSyncManager syncManager; private SpectatorSyncManager syncManager;
private Dictionary<ISpectatorPlayerClock, int> clocksById; private Dictionary<SpectatorPlayerClock, int> clocksById;
private ISpectatorPlayerClock player1; private SpectatorPlayerClock player1;
private ISpectatorPlayerClock player2; private SpectatorPlayerClock player2;
[SetUp] [SetUp]
public void Setup() public void Setup()
{ {
syncManager = new CatchUpSyncManager(master = new GameplayClockContainer(new TestManualClock())); syncManager = new SpectatorSyncManager(master = new GameplayClockContainer(new TestManualClock()));
player1 = syncManager.CreateManagedClock(); player1 = syncManager.CreateManagedClock();
player2 = syncManager.CreateManagedClock(); player2 = syncManager.CreateManagedClock();
clocksById = new Dictionary<ISpectatorPlayerClock, int> clocksById = new Dictionary<SpectatorPlayerClock, int>
{ {
{ player1, 1 }, { player1, 1 },
{ player2, 2 } { player2, 2 }
@ -64,7 +64,7 @@ namespace osu.Game.Tests.OnlinePlay
public void TestReadyPlayersStartWhenReadyForMaximumDelayTime() public void TestReadyPlayersStartWhenReadyForMaximumDelayTime()
{ {
setWaiting(() => player1, false); 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(() => player1, true);
assertPlayerClockState(() => player2, false); assertPlayerClockState(() => player2, false);
} }
@ -74,7 +74,7 @@ namespace osu.Game.Tests.OnlinePlay
{ {
setAllWaiting(false); setAllWaiting(false);
setMasterTime(CatchUpSyncManager.SYNC_TARGET + 1); setMasterTime(SpectatorSyncManager.SYNC_TARGET + 1);
assertCatchingUp(() => player1, false); assertCatchingUp(() => player1, false);
} }
@ -83,7 +83,7 @@ namespace osu.Game.Tests.OnlinePlay
{ {
setAllWaiting(false); setAllWaiting(false);
setMasterTime(CatchUpSyncManager.MAX_SYNC_OFFSET + 1); setMasterTime(SpectatorSyncManager.MAX_SYNC_OFFSET + 1);
assertCatchingUp(() => player1, true); assertCatchingUp(() => player1, true);
assertCatchingUp(() => player2, true); assertCatchingUp(() => player2, true);
} }
@ -93,8 +93,8 @@ namespace osu.Game.Tests.OnlinePlay
{ {
setAllWaiting(false); setAllWaiting(false);
setMasterTime(CatchUpSyncManager.MAX_SYNC_OFFSET + 1); setMasterTime(SpectatorSyncManager.MAX_SYNC_OFFSET + 1);
setPlayerClockTime(() => player1, CatchUpSyncManager.SYNC_TARGET + 1); setPlayerClockTime(() => player1, SpectatorSyncManager.SYNC_TARGET + 1);
assertCatchingUp(() => player1, true); assertCatchingUp(() => player1, true);
} }
@ -103,8 +103,8 @@ namespace osu.Game.Tests.OnlinePlay
{ {
setAllWaiting(false); setAllWaiting(false);
setMasterTime(CatchUpSyncManager.MAX_SYNC_OFFSET + 2); setMasterTime(SpectatorSyncManager.MAX_SYNC_OFFSET + 2);
setPlayerClockTime(() => player1, CatchUpSyncManager.SYNC_TARGET); setPlayerClockTime(() => player1, SpectatorSyncManager.SYNC_TARGET);
assertCatchingUp(() => player1, false); assertCatchingUp(() => player1, false);
assertCatchingUp(() => player2, true); assertCatchingUp(() => player2, true);
} }
@ -114,7 +114,7 @@ namespace osu.Game.Tests.OnlinePlay
{ {
setAllWaiting(false); setAllWaiting(false);
setPlayerClockTime(() => player1, -CatchUpSyncManager.SYNC_TARGET); setPlayerClockTime(() => player1, -SpectatorSyncManager.SYNC_TARGET);
assertCatchingUp(() => player1, false); assertCatchingUp(() => player1, false);
assertPlayerClockState(() => player1, true); assertPlayerClockState(() => player1, true);
} }
@ -124,7 +124,7 @@ namespace osu.Game.Tests.OnlinePlay
{ {
setAllWaiting(false); 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. // This is a silent catchup, where IsCatchingUp = false but IsRunning = false also.
assertCatchingUp(() => player1, false); assertCatchingUp(() => player1, false);
@ -145,7 +145,7 @@ namespace osu.Game.Tests.OnlinePlay
assertPlayerClockState(() => player1, false); assertPlayerClockState(() => player1, false);
} }
private void setWaiting(Func<ISpectatorPlayerClock> playerClock, bool waiting) private void setWaiting(Func<SpectatorPlayerClock> playerClock, bool waiting)
=> AddStep($"set player clock {clocksById[playerClock()]} waiting = {waiting}", () => playerClock().WaitingOnFrames.Value = 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}", () => private void setAllWaiting(bool waiting) => AddStep($"set all player clocks waiting = {waiting}", () =>
@ -160,13 +160,13 @@ namespace osu.Game.Tests.OnlinePlay
/// <summary> /// <summary>
/// clock.Time = master.Time - offsetFromMaster /// clock.Time = master.Time - offsetFromMaster
/// </summary> /// </summary>
private void setPlayerClockTime(Func<ISpectatorPlayerClock> playerClock, double offsetFromMaster) private void setPlayerClockTime(Func<SpectatorPlayerClock> playerClock, double offsetFromMaster)
=> AddStep($"set player clock {clocksById[playerClock()]} = master - {offsetFromMaster}", () => playerClock().Seek(master.CurrentTime - offsetFromMaster)); => AddStep($"set player clock {clocksById[playerClock()]} = master - {offsetFromMaster}", () => playerClock().Seek(master.CurrentTime - offsetFromMaster));
private void assertCatchingUp(Func<ISpectatorPlayerClock> playerClock, bool catchingUp) => private void assertCatchingUp(Func<SpectatorPlayerClock> playerClock, bool catchingUp) =>
AddAssert($"player clock {clocksById[playerClock()]} {(catchingUp ? "is" : "is not")} catching up", () => playerClock().IsCatchingUp == catchingUp); AddAssert($"player clock {clocksById[playerClock()]} {(catchingUp ? "is" : "is not")} catching up", () => playerClock().IsCatchingUp == catchingUp);
private void assertPlayerClockState(Func<ISpectatorPlayerClock> playerClock, bool running) private void assertPlayerClockState(Func<SpectatorPlayerClock> playerClock, bool running)
=> AddAssert($"player clock {clocksById[playerClock()]} {(running ? "is" : "is not")} running", () => playerClock().IsRunning == running); => AddAssert($"player clock {clocksById[playerClock()]} {(running ? "is" : "is not")} running", () => playerClock().IsRunning == running);
private class TestManualClock : ManualClock, IAdjustableClock private class TestManualClock : ManualClock, IAdjustableClock

View File

@ -202,7 +202,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
checkPausedInstant(PLAYER_2_ID, true); checkPausedInstant(PLAYER_2_ID, true);
// Wait for the start delay seconds... // 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. // Player 1 should start playing by itself, player 2 should remain paused.
checkPausedInstant(PLAYER_1_ID, false); checkPausedInstant(PLAYER_1_ID, false);
@ -318,7 +318,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
loadSpectateScreen(); loadSpectateScreen();
sendFrames(PLAYER_1_ID, 300); 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); checkPaused(PLAYER_1_ID, false);
sendFrames(PLAYER_2_ID, 300); sendFrames(PLAYER_2_ID, 300);

View File

@ -1,37 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Timing;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
/// <summary>
/// A clock which is used by <see cref="MultiSpectatorPlayer"/>s and managed by an <see cref="ISyncManager"/>.
/// </summary>
public interface ISpectatorPlayerClock : IFrameBasedClock, IAdjustableClock
{
/// <summary>
/// Starts this <see cref="ISpectatorPlayerClock"/>.
/// </summary>
new void Start();
/// <summary>
/// Stops this <see cref="ISpectatorPlayerClock"/>.
/// </summary>
new void Stop();
/// <summary>
/// Whether this clock is waiting on frames to continue playback.
/// </summary>
Bindable<bool> WaitingOnFrames { get; }
/// <summary>
/// Whether this clock is behind the master clock and running at a higher rate to catch up to it.
/// </summary>
/// <remarks>
/// Of note, this will be false if this clock is *ahead* of the master clock.
/// </remarks>
bool IsCatchingUp { get; set; }
}
}

View File

@ -1,42 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Bindables;
using osu.Game.Screens.Play;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
/// <summary>
/// Manages the synchronisation between one or more <see cref="ISpectatorPlayerClock"/>s in relation to a master clock.
/// </summary>
public interface ISyncManager
{
/// <summary>
/// An event which is invoked when gameplay is ready to start.
/// </summary>
event Action? ReadyToStart;
/// <summary>
/// The master clock which player clocks should synchronise to.
/// </summary>
GameplayClockContainer MasterClock { get; }
/// <summary>
/// An event which is invoked when the state of <see cref="MasterClock"/> is changed.
/// </summary>
IBindable<MasterClockState> MasterState { get; }
/// <summary>
/// Create a new managed <see cref="ISpectatorPlayerClock"/>.
/// </summary>
/// <returns>The newly created <see cref="ISpectatorPlayerClock"/>.</returns>
ISpectatorPlayerClock CreateManagedClock();
/// <summary>
/// Removes an <see cref="ISpectatorPlayerClock"/>, stopping it from being managed by this <see cref="ISyncManager"/>.
/// </summary>
/// <param name="clock">The <see cref="ISpectatorPlayerClock"/> to remove.</param>
void RemoveManagedClock(ISpectatorPlayerClock clock);
}
}

View File

@ -15,14 +15,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
public class MultiSpectatorPlayer : SpectatorPlayer public class MultiSpectatorPlayer : SpectatorPlayer
{ {
private readonly Bindable<bool> waitingOnFrames = new Bindable<bool>(true); private readonly Bindable<bool> waitingOnFrames = new Bindable<bool>(true);
private readonly ISpectatorPlayerClock spectatorPlayerClock; private readonly SpectatorPlayerClock spectatorPlayerClock;
/// <summary> /// <summary>
/// Creates a new <see cref="MultiSpectatorPlayer"/>. /// Creates a new <see cref="MultiSpectatorPlayer"/>.
/// </summary> /// </summary>
/// <param name="score">The score containing the player's replay.</param> /// <param name="score">The score containing the player's replay.</param>
/// <param name="spectatorPlayerClock">The clock controlling the gameplay running state.</param> /// <param name="spectatorPlayerClock">The clock controlling the gameplay running state.</param>
public MultiSpectatorPlayer(Score score, ISpectatorPlayerClock spectatorPlayerClock) public MultiSpectatorPlayer(Score score, SpectatorPlayerClock spectatorPlayerClock)
: base(score, new PlayerConfiguration { AllowUserInteraction = false }) : base(score, new PlayerConfiguration { AllowUserInteraction = false })
{ {
this.spectatorPlayerClock = spectatorPlayerClock; this.spectatorPlayerClock = spectatorPlayerClock;
@ -40,9 +40,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
protected override void Update() 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. // 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 (GameplayClockContainer.SourceClock.IsRunning)
if (catchUpClock.IsRunning)
GameplayClockContainer.Start(); GameplayClockContainer.Start();
else else
GameplayClockContainer.Stop(); GameplayClockContainer.Stop();

View File

@ -8,6 +8,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
@ -47,7 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
private readonly PlayerArea[] instances; private readonly PlayerArea[] instances;
private MasterGameplayClockContainer masterClockContainer = null!; private MasterGameplayClockContainer masterClockContainer = null!;
private ISyncManager syncManager = null!; private SpectatorSyncManager syncManager = null!;
private PlayerGrid grid = null!; private PlayerGrid grid = null!;
private MultiSpectatorLeaderboard leaderboard = null!; private MultiSpectatorLeaderboard leaderboard = null!;
private PlayerArea? currentAudioSource; private PlayerArea? currentAudioSource;
@ -80,7 +81,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
InternalChildren = new[] InternalChildren = new[]
{ {
(Drawable)(syncManager = new CatchUpSyncManager(masterClockContainer)), (Drawable)(syncManager = new SpectatorSyncManager(masterClockContainer)),
masterClockContainer.WithChild(new GridContainer masterClockContainer.WithChild(new GridContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -168,7 +169,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
if (!isCandidateAudioSource(currentAudioSource?.GameplayClock)) if (!isCandidateAudioSource(currentAudioSource?.GameplayClock))
{ {
currentAudioSource = instances.Where(i => isCandidateAudioSource(i.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(); .FirstOrDefault();
foreach (var instance in instances) 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; => clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames.Value;
private void onReadyToStart() private void onReadyToStart()
@ -198,6 +199,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
private void onMasterStateChanged(ValueChangedEvent<MasterClockState> state) private void onMasterStateChanged(ValueChangedEvent<MasterClockState> state)
{ {
Logger.Log($"{nameof(MultiSpectatorScreen)}'s master clock become {state.NewValue}");
switch (state.NewValue) switch (state.NewValue)
{ {
case MasterClockState.Synchronised: case MasterClockState.Synchronised:

View File

@ -38,9 +38,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
public readonly int UserId; public readonly int UserId;
/// <summary> /// <summary>
/// The <see cref="ISpectatorPlayerClock"/> used to control the gameplay running state of a loaded <see cref="Player"/>. /// The <see cref="SpectatorPlayerClock"/> used to control the gameplay running state of a loaded <see cref="Player"/>.
/// </summary> /// </summary>
public readonly ISpectatorPlayerClock GameplayClock; public readonly SpectatorPlayerClock GameplayClock;
/// <summary> /// <summary>
/// The currently-loaded score. /// The currently-loaded score.
@ -55,7 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
private readonly LoadingLayer loadingLayer; private readonly LoadingLayer loadingLayer;
private OsuScreenStack? stack; private OsuScreenStack? stack;
public PlayerArea(int userId, ISpectatorPlayerClock clock) public PlayerArea(int userId, SpectatorPlayerClock clock)
{ {
UserId = userId; UserId = userId;
GameplayClock = clock; GameplayClock = clock;

View File

@ -4,44 +4,58 @@
using System; using System;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Screens.Play;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{ {
/// <summary> /// <summary>
/// A <see cref="ISpectatorPlayerClock"/> which catches up using rate adjustment. /// A clock which catches up using rate adjustment.
/// </summary> /// </summary>
public class CatchUpSpectatorPlayerClock : ISpectatorPlayerClock public class SpectatorPlayerClock : IFrameBasedClock, IAdjustableClock
{ {
/// <summary> /// <summary>
/// The catch up rate. /// The catch up rate.
/// </summary> /// </summary>
public const double CATCHUP_RATE = 2; public const double CATCHUP_RATE = 2;
public readonly IFrameBasedClock Source; private readonly GameplayClockContainer masterClock;
public double CurrentTime { get; private set; } public double CurrentTime { get; private set; }
public bool IsRunning { get; private set; } /// <summary>
/// Whether this clock is waiting on frames to continue playback.
/// </summary>
public Bindable<bool> WaitingOnFrames { get; } = new Bindable<bool>(true);
public CatchUpSpectatorPlayerClock(IFrameBasedClock source) /// <summary>
/// Whether this clock is behind the master clock and running at a higher rate to catch up to it.
/// </summary>
/// <remarks>
/// Of note, this will be false if this clock is *ahead* of the master clock.
/// </remarks>
public bool IsCatchingUp { get; set; }
/// <summary>
/// Whether this spectator clock should be running.
/// Use instead of <see cref="Start"/> / <see cref="Stop"/> to control time.
/// </summary>
public bool IsRunning { get; set; }
public SpectatorPlayerClock(GameplayClockContainer masterClock)
{ {
Source = source; this.masterClock = masterClock;
} }
public void Reset() => CurrentTime = 0; public void Reset() => CurrentTime = 0;
public void Start() => IsRunning = true; public void Start()
public void Stop() => IsRunning = false;
void IAdjustableClock.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) public bool Seek(double position)
@ -69,16 +83,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
ElapsedFrameTime = 0; ElapsedFrameTime = 0;
FramesPerSecond = 0; FramesPerSecond = 0;
Source.ProcessFrame(); masterClock.ProcessFrame();
if (IsRunning) if (IsRunning)
{ {
double elapsedSource = Source.ElapsedFrameTime; double elapsedSource = masterClock.ElapsedFrameTime;
double elapsed = elapsedSource * Rate; double elapsed = elapsedSource * Rate;
CurrentTime += elapsed; CurrentTime += elapsed;
ElapsedFrameTime = 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 double FramesPerSecond { get; private set; }
public FrameTimeInfo TimeInfo => new FrameTimeInfo { Elapsed = ElapsedFrameTime, Current = CurrentTime }; public FrameTimeInfo TimeInfo => new FrameTimeInfo { Elapsed = ElapsedFrameTime, Current = CurrentTime };
public Bindable<bool> WaitingOnFrames { get; } = new Bindable<bool>(true);
public bool IsCatchingUp { get; set; }
} }
} }

View File

@ -11,9 +11,9 @@ using osu.Game.Screens.Play;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{ {
/// <summary> /// <summary>
/// A <see cref="ISyncManager"/> which synchronises de-synced player clocks through catchup. /// Manages the synchronisation between one or more <see cref="SpectatorPlayerClock"/>s in relation to a master clock.
/// </summary> /// </summary>
public class CatchUpSyncManager : Component, ISyncManager public class SpectatorSyncManager : Component
{ {
/// <summary> /// <summary>
/// The offset from the master clock to which player clocks should remain within to be considered in-sync. /// 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
/// </summary> /// </summary>
public const double MAXIMUM_START_DELAY = 15000; public const double MAXIMUM_START_DELAY = 15000;
/// <summary>
/// An event which is invoked when gameplay is ready to start.
/// </summary>
public event Action? ReadyToStart; public event Action? ReadyToStart;
/// <summary>
/// The catch-up state of the master clock.
/// </summary>
public IBindable<MasterClockState> MasterState => masterState;
public double CurrentMasterTime => masterClock.CurrentTime;
/// <summary> /// <summary>
/// The master clock which is used to control the timing of all player clocks clocks. /// The master clock which is used to control the timing of all player clocks clocks.
/// </summary> /// </summary>
public GameplayClockContainer MasterClock { get; } private readonly GameplayClockContainer masterClock;
public IBindable<MasterClockState> MasterState => masterState;
/// <summary> /// <summary>
/// The player clocks. /// The player clocks.
/// </summary> /// </summary>
private readonly List<ISpectatorPlayerClock> playerClocks = new List<ISpectatorPlayerClock>(); private readonly List<SpectatorPlayerClock> playerClocks = new List<SpectatorPlayerClock>();
private readonly Bindable<MasterClockState> masterState = new Bindable<MasterClockState>(); private readonly Bindable<MasterClockState> masterState = new Bindable<MasterClockState>();
private bool hasStarted; private bool hasStarted;
private double? firstStartAttemptTime; private double? firstStartAttemptTime;
public CatchUpSyncManager(GameplayClockContainer master) public SpectatorSyncManager(GameplayClockContainer master)
{ {
MasterClock = master; masterClock = master;
} }
public ISpectatorPlayerClock CreateManagedClock() /// <summary>
/// Create a new managed <see cref="SpectatorPlayerClock"/>.
/// </summary>
/// <returns>The newly created <see cref="SpectatorPlayerClock"/>.</returns>
public SpectatorPlayerClock CreateManagedClock()
{ {
var clock = new CatchUpSpectatorPlayerClock(MasterClock); var clock = new SpectatorPlayerClock(masterClock);
playerClocks.Add(clock); playerClocks.Add(clock);
return clock; return clock;
} }
public void RemoveManagedClock(ISpectatorPlayerClock clock) /// <summary>
/// Removes an <see cref="SpectatorPlayerClock"/>, stopping it from being managed by this <see cref="SpectatorSyncManager"/>.
/// </summary>
/// <param name="clock">The <see cref="SpectatorPlayerClock"/> to remove.</param>
public void RemoveManagedClock(SpectatorPlayerClock clock)
{ {
playerClocks.Remove(clock); playerClocks.Remove(clock);
clock.Stop(); clock.IsRunning = false;
} }
protected override void Update() 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. // Ensure all player clocks are stopped until the start succeeds.
foreach (var clock in playerClocks) foreach (var clock in playerClocks)
clock.Stop(); clock.IsRunning = false;
return; 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. // 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). // 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. // 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. // 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 // 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). // when it is required to be running (ie. if all players are ahead of the master).
clock.IsCatchingUp = false; clock.IsCatchingUp = false;
clock.Stop(); clock.IsRunning = false;
continue; continue;
} }
// Make sure the player clock is running if it can. // Make sure the player clock is running if it can.
if (!clock.WaitingOnFrames.Value) clock.IsRunning = !clock.WaitingOnFrames.Value;
clock.Start();
else
clock.Stop();
if (clock.IsCatchingUp) if (clock.IsCatchingUp)
{ {