1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 09:17:51 +08:00

Merge branch 'master' into copyexternalurl-dialog

This commit is contained in:
Bartłomiej Dach 2022-08-24 21:21:11 +02:00 committed by GitHub
commit 968f656585
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 314 additions and 371 deletions

View File

@ -19,20 +19,20 @@ namespace osu.Game.Tests.OnlinePlay
public class TestSceneCatchUpSyncManager : OsuTestScene
{
private GameplayClockContainer master;
private CatchUpSyncManager syncManager;
private SpectatorSyncManager syncManager;
private Dictionary<ISpectatorPlayerClock, int> clocksById;
private ISpectatorPlayerClock player1;
private ISpectatorPlayerClock player2;
private Dictionary<SpectatorPlayerClock, int> 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<ISpectatorPlayerClock, int>
clocksById = new Dictionary<SpectatorPlayerClock, int>
{
{ 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,13 +145,13 @@ namespace osu.Game.Tests.OnlinePlay
assertPlayerClockState(() => player1, false);
}
private void setWaiting(Func<ISpectatorPlayerClock> playerClock, bool waiting)
=> AddStep($"set player clock {clocksById[playerClock()]} waiting = {waiting}", () => playerClock().WaitingOnFrames.Value = waiting);
private void setWaiting(Func<SpectatorPlayerClock> playerClock, bool waiting)
=> AddStep($"set player clock {clocksById[playerClock()]} waiting = {waiting}", () => playerClock().WaitingOnFrames = waiting);
private void setAllWaiting(bool waiting) => AddStep($"set all player clocks waiting = {waiting}", () =>
{
player1.WaitingOnFrames.Value = waiting;
player2.WaitingOnFrames.Value = waiting;
player1.WaitingOnFrames = waiting;
player2.WaitingOnFrames = waiting;
});
private void setMasterTime(double time)
@ -160,13 +160,13 @@ namespace osu.Game.Tests.OnlinePlay
/// <summary>
/// clock.Time = master.Time - offsetFromMaster
/// </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));
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);
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);
private class TestManualClock : ManualClock, IAdjustableClock

View File

@ -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);

View File

@ -14,6 +14,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
@ -70,85 +71,90 @@ namespace osu.Game.Overlays.Profile.Header
Masking = true,
CornerRadius = avatar_size * 0.25f,
},
new Container
new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Padding = new MarginPadding { Left = 10 },
Children = new Drawable[]
Child = new Container
{
new FillFlowContainer
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Padding = new MarginPadding { Left = 10 },
Children = new Drawable[]
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
new FillFlowContainer
{
new FillFlowContainer
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
new FillFlowContainer
{
usernameText = new OsuSpriteText
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Regular)
},
openUserExternally = new ExternalLinkButton
{
Margin = new MarginPadding { Left = 5 },
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
}
},
titleText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 18, weight: FontWeight.Regular)
},
}
},
new FillFlowContainer
{
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
supporterTag = new SupporterIcon
{
Height = 20,
Margin = new MarginPadding { Top = 5 }
},
new Box
{
RelativeSizeAxes = Axes.X,
Height = 1.5f,
Margin = new MarginPadding { Top = 10 },
Colour = colourProvider.Light1,
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Top = 5 },
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
userFlag = new UpdateableFlag
{
Size = new Vector2(28, 20),
ShowPlaceholderOnUnknown = false,
},
userCountryText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 17.5f, weight: FontWeight.Regular),
Margin = new MarginPadding { Left = 10 },
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Colour = colourProvider.Light1,
usernameText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Regular)
},
openUserExternally = new ExternalLinkButton
{
Margin = new MarginPadding { Left = 5 },
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
}
}
},
},
titleText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 18, weight: FontWeight.Regular)
},
}
},
new FillFlowContainer
{
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
supporterTag = new SupporterIcon
{
Height = 20,
Margin = new MarginPadding { Top = 5 }
},
new Box
{
RelativeSizeAxes = Axes.X,
Height = 1.5f,
Margin = new MarginPadding { Top = 10 },
Colour = colourProvider.Light1,
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Top = 5 },
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
userFlag = new UpdateableFlag
{
Size = new Vector2(28, 20),
ShowPlaceholderOnUnknown = false,
},
userCountryText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 17.5f, weight: FontWeight.Regular),
Margin = new MarginPadding { Left = 10 },
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Colour = colourProvider.Light1,
}
}
},
}
}
}
}

View File

@ -1,95 +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.Framework.Timing;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
/// <summary>
/// A <see cref="ISpectatorPlayerClock"/> which catches up using rate adjustment.
/// </summary>
public class CatchUpSpectatorPlayerClock : ISpectatorPlayerClock
{
/// <summary>
/// The catch up rate.
/// </summary>
public const double CATCHUP_RATE = 2;
public readonly IFrameBasedClock Source;
public double CurrentTime { get; private set; }
public bool IsRunning { get; private set; }
public CatchUpSpectatorPlayerClock(IFrameBasedClock source)
{
Source = source;
}
public void Reset() => CurrentTime = 0;
public void Start() => IsRunning = true;
public void Stop() => IsRunning = false;
void IAdjustableClock.Start()
{
// Our running state should only be managed by an ISyncManager, ignore calls from external sources.
}
void IAdjustableClock.Stop()
{
// Our running state should only be managed by an ISyncManager, ignore calls from external sources.
}
public bool Seek(double position)
{
CurrentTime = position;
return true;
}
public void ResetSpeedAdjustments()
{
}
public double Rate => IsCatchingUp ? CATCHUP_RATE : 1;
double IAdjustableClock.Rate
{
get => Rate;
set => throw new NotSupportedException();
}
double IClock.Rate => Rate;
public void ProcessFrame()
{
ElapsedFrameTime = 0;
FramesPerSecond = 0;
Source.ProcessFrame();
if (IsRunning)
{
double elapsedSource = Source.ElapsedFrameTime;
double elapsed = elapsedSource * Rate;
CurrentTime += elapsed;
ElapsedFrameTime = elapsed;
FramesPerSecond = Source.FramesPerSecond;
}
}
public double ElapsedFrameTime { get; private set; }
public double FramesPerSecond { get; private set; }
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

@ -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

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
@ -14,15 +13,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// </summary>
public class MultiSpectatorPlayer : SpectatorPlayer
{
private readonly Bindable<bool> waitingOnFrames = new Bindable<bool>(true);
private readonly ISpectatorPlayerClock spectatorPlayerClock;
private readonly SpectatorPlayerClock spectatorPlayerClock;
/// <summary>
/// Creates a new <see cref="MultiSpectatorPlayer"/>.
/// </summary>
/// <param name="score">The score containing the player's replay.</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 })
{
this.spectatorPlayerClock = spectatorPlayerClock;
@ -31,8 +29,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
[BackgroundDependencyLoader]
private void load()
{
spectatorPlayerClock.WaitingOnFrames.BindTo(waitingOnFrames);
HUDOverlay.PlayerSettingsOverlay.Expire();
HUDOverlay.HoldToQuit.Expire();
}
@ -40,9 +36,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();
@ -55,7 +49,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
base.UpdateAfterChildren();
// This is required because the frame stable clock is set to WaitingOnFrames = false for one frame.
waitingOnFrames.Value = DrawableRuleset.FrameStableClock.WaitingOnFrames.Value || Score.Replay.Frames.Count == 0;
spectatorPlayerClock.WaitingOnFrames = DrawableRuleset.FrameStableClock.WaitingOnFrames.Value || Score.Replay.Frames.Count == 0;
}
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart)

View File

@ -4,11 +4,9 @@
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
@ -47,11 +45,10 @@ 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;
private bool canStartMasterClock;
private readonly Room room;
private readonly MultiplayerRoomUser[] users;
@ -76,50 +73,54 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
FillFlowContainer leaderboardFlow;
Container scoreDisplayContainer;
masterClockContainer = CreateMasterGameplayClockContainer(Beatmap.Value);
InternalChildren = new[]
InternalChildren = new Drawable[]
{
(Drawable)(syncManager = new CatchUpSyncManager(masterClockContainer)),
masterClockContainer.WithChild(new GridContainer
masterClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0)
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
Content = new[]
Child = new GridContainer
{
new Drawable[]
RelativeSizeAxes = Axes.Both,
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
Content = new[]
{
scoreDisplayContainer = new Container
new Drawable[]
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
},
},
new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
Content = new[]
scoreDisplayContainer = new Container
{
new Drawable[]
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
},
},
new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
Content = new[]
{
leaderboardFlow = new FillFlowContainer
new Drawable[]
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5)
},
grid = new PlayerGrid { RelativeSizeAxes = Axes.Both }
leaderboardFlow = new FillFlowContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5)
},
grid = new PlayerGrid { RelativeSizeAxes = Axes.Both }
}
}
}
}
}
}
})
},
syncManager = new SpectatorSyncManager(masterClockContainer)
{
ReadyToStart = performInitialSeek,
}
};
for (int i = 0; i < Users.Count; i++)
@ -156,9 +157,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
base.LoadComplete();
masterClockContainer.Reset();
syncManager.ReadyToStart += onReadyToStart;
syncManager.MasterState.BindValueChanged(onMasterStateChanged, true);
}
protected override void Update()
@ -168,7 +166,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,10 +174,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
}
}
private bool isCandidateAudioSource(ISpectatorPlayerClock? clock)
=> clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames.Value;
private bool isCandidateAudioSource(SpectatorPlayerClock? clock)
=> clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames;
private void onReadyToStart()
private void performInitialSeek()
{
// Seek the master clock to the gameplay time.
// This is chosen as the first available frame in the players' replays, which matches the seek by each individual SpectatorPlayer.
@ -191,25 +189,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
masterClockContainer.StartTime = startTime;
masterClockContainer.Reset(true);
// Although the clock has been started, this flag is set to allow for later synchronisation state changes to also be able to start it.
canStartMasterClock = true;
}
private void onMasterStateChanged(ValueChangedEvent<MasterClockState> state)
{
switch (state.NewValue)
{
case MasterClockState.Synchronised:
if (canStartMasterClock)
masterClockContainer.Start();
break;
case MasterClockState.TooFarAhead:
masterClockContainer.Stop();
break;
}
}
protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState)
@ -251,7 +230,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
return base.OnBackButton();
}
protected virtual MasterGameplayClockContainer CreateMasterGameplayClockContainer(WorkingBeatmap beatmap) => new MasterGameplayClockContainer(beatmap, 0);
}
}

View File

@ -38,9 +38,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
public readonly int UserId;
/// <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>
public readonly ISpectatorPlayerClock GameplayClock;
public readonly SpectatorPlayerClock GameplayClock;
/// <summary>
/// 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;

View File

@ -0,0 +1,100 @@
// 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.Timing;
using osu.Game.Screens.Play;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
/// <summary>
/// A clock which catches up using rate adjustment.
/// </summary>
public class SpectatorPlayerClock : IFrameBasedClock, IAdjustableClock
{
/// <summary>
/// The catch up rate.
/// </summary>
private const double catchup_rate = 2;
private readonly GameplayClockContainer masterClock;
public double CurrentTime { get; private set; }
/// <summary>
/// Whether this clock is waiting on frames to continue playback.
/// </summary>
public bool WaitingOnFrames { get; set; } = true;
/// <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)
{
this.masterClock = masterClock;
}
public void Reset() => CurrentTime = 0;
public void Start()
{
// Our running state should only be managed by SpectatorSyncManager via IsRunning.
}
public void Stop()
{
// Our running state should only be managed by an SpectatorSyncManager via IsRunning.
}
public bool Seek(double position)
{
CurrentTime = position;
return true;
}
public void ResetSpeedAdjustments()
{
}
public double Rate
{
get => IsCatchingUp ? catchup_rate : 1;
set => throw new NotImplementedException();
}
public void ProcessFrame()
{
if (IsRunning)
{
double elapsedSource = masterClock.ElapsedFrameTime;
double elapsed = elapsedSource * Rate;
CurrentTime += elapsed;
ElapsedFrameTime = elapsed;
FramesPerSecond = masterClock.FramesPerSecond;
}
else
{
ElapsedFrameTime = 0;
FramesPerSecond = 0;
}
}
public double ElapsedFrameTime { get; private set; }
public double FramesPerSecond { get; private set; }
public FrameTimeInfo TimeInfo => new FrameTimeInfo { Elapsed = ElapsedFrameTime, Current = CurrentTime };
}
}

View File

@ -4,16 +4,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Screens.Play;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
/// <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>
public class CatchUpSyncManager : Component, ISyncManager
public class SpectatorSyncManager : Component
{
/// <summary>
/// The offset from the master clock to which player clocks should remain within to be considered in-sync.
@ -30,41 +30,53 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// </summary>
public const double MAXIMUM_START_DELAY = 15000;
public event Action? ReadyToStart;
/// <summary>
/// An event which is invoked when gameplay is ready to start.
/// </summary>
public Action? ReadyToStart;
public double CurrentMasterTime => masterClock.CurrentTime;
/// <summary>
/// The master clock which is used to control the timing of all player clocks clocks.
/// </summary>
public GameplayClockContainer MasterClock { get; }
public IBindable<MasterClockState> MasterState => masterState;
private readonly GameplayClockContainer masterClock;
/// <summary>
/// The player clocks.
/// </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 MasterClockState masterState = MasterClockState.Synchronised;
private bool hasStarted;
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);
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);
clock.Stop();
clock.IsRunning = false;
}
protected override void Update()
@ -75,7 +87,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;
}
@ -95,7 +107,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
if (playerClocks.Count == 0)
return false;
int readyCount = playerClocks.Count(s => !s.WaitingOnFrames.Value);
int readyCount = playerClocks.Count(s => !s.WaitingOnFrames);
if (readyCount == playerClocks.Count)
return performStart();
@ -128,7 +140,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 +149,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;
if (clock.IsCatchingUp)
{
@ -167,8 +176,26 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// </summary>
private void updateMasterState()
{
bool anyInSync = playerClocks.Any(s => !s.IsCatchingUp);
masterState.Value = anyInSync ? MasterClockState.Synchronised : MasterClockState.TooFarAhead;
MasterClockState newState = playerClocks.Any(s => !s.IsCatchingUp) ? MasterClockState.Synchronised : MasterClockState.TooFarAhead;
if (masterState == newState)
return;
masterState = newState;
Logger.Log($"{nameof(SpectatorSyncManager)}'s master clock become {masterState}");
switch (masterState)
{
case MasterClockState.Synchronised:
if (hasStarted)
masterClock.Start();
break;
case MasterClockState.TooFarAhead:
masterClock.Stop();
break;
}
}
}
}

View File

@ -55,8 +55,6 @@ namespace osu.Game.Screens.Select.Carousel
match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(BeatmapInfo.Metadata.Artist) ||
criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode);
match &= criteria.Sort != SortMode.DateRanked || BeatmapInfo.BeatmapSet?.DateRanked != null;
match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(BeatmapInfo.StarRating);
if (match && criteria.SearchTerms.Length > 0)

View File

@ -99,6 +99,13 @@ namespace osu.Game.Screens.Select.Carousel
case SortMode.Difficulty:
return compareUsingAggregateMax(otherSet, b => b.StarRating);
case SortMode.DateSubmitted:
// Beatmaps which have no submitted date should already be filtered away in this mode.
if (BeatmapSet.DateSubmitted == null || otherSet.BeatmapSet.DateSubmitted == null)
return 0;
return otherSet.BeatmapSet.DateSubmitted.Value.CompareTo(BeatmapSet.DateSubmitted.Value);
}
}
@ -122,7 +129,12 @@ namespace osu.Game.Screens.Select.Carousel
public override void Filter(FilterCriteria criteria)
{
base.Filter(criteria);
Filtered.Value = Items.All(i => i.Filtered.Value);
bool match = Items.All(i => i.Filtered.Value);
match &= criteria.Sort != SortMode.DateRanked || BeatmapSet?.DateRanked != null;
match &= criteria.Sort != SortMode.DateSubmitted || BeatmapSet?.DateSubmitted != null;
Filtered.Value = match;
}
public override string ToString() => BeatmapSet.ToString();

View File

@ -20,6 +20,9 @@ namespace osu.Game.Screens.Select.Filter
[LocalisableDescription(typeof(SortStrings), nameof(SortStrings.ArtistTracksBpm))]
BPM,
[Description("Date Submitted")]
DateSubmitted,
[Description("Date Added")]
DateAdded,