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

This commit is contained in:
Andrew Hong 2022-08-23 12:00:30 -04:00 committed by GitHub
commit 3cedd1cd1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 75 additions and 120 deletions

View File

@ -4,11 +4,13 @@
#nullable disable
using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.OnlinePlay
@ -16,20 +18,34 @@ namespace osu.Game.Tests.OnlinePlay
[HeadlessTest]
public class TestSceneCatchUpSyncManager : OsuTestScene
{
private TestManualClock master;
private GameplayClockContainer master;
private CatchUpSyncManager syncManager;
private TestSpectatorPlayerClock player1;
private TestSpectatorPlayerClock player2;
private Dictionary<ISpectatorPlayerClock, int> clocksById;
private ISpectatorPlayerClock player1;
private ISpectatorPlayerClock player2;
[SetUp]
public void Setup()
{
syncManager = new CatchUpSyncManager(master = new TestManualClock());
syncManager.AddPlayerClock(player1 = new TestSpectatorPlayerClock(1));
syncManager.AddPlayerClock(player2 = new TestSpectatorPlayerClock(2));
syncManager = new CatchUpSyncManager(master = new GameplayClockContainer(new TestManualClock()));
player1 = syncManager.CreateManagedClock();
player2 = syncManager.CreateManagedClock();
Schedule(() => Child = syncManager);
clocksById = new Dictionary<ISpectatorPlayerClock, int>
{
{ player1, 1 },
{ player2, 2 }
};
Schedule(() =>
{
Children = new Drawable[]
{
syncManager,
master
};
});
}
[Test]
@ -129,8 +145,8 @@ namespace osu.Game.Tests.OnlinePlay
assertPlayerClockState(() => player1, false);
}
private void setWaiting(Func<TestSpectatorPlayerClock> playerClock, bool waiting)
=> AddStep($"set player clock {playerClock().Id} waiting = {waiting}", () => playerClock().WaitingOnFrames.Value = waiting);
private void setWaiting(Func<ISpectatorPlayerClock> 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}", () =>
{
@ -144,51 +160,14 @@ namespace osu.Game.Tests.OnlinePlay
/// <summary>
/// clock.Time = master.Time - offsetFromMaster
/// </summary>
private void setPlayerClockTime(Func<TestSpectatorPlayerClock> playerClock, double offsetFromMaster)
=> AddStep($"set player clock {playerClock().Id} = master - {offsetFromMaster}", () => playerClock().Seek(master.CurrentTime - offsetFromMaster));
private void setPlayerClockTime(Func<ISpectatorPlayerClock> playerClock, double offsetFromMaster)
=> AddStep($"set player clock {clocksById[playerClock()]} = master - {offsetFromMaster}", () => playerClock().Seek(master.CurrentTime - offsetFromMaster));
private void assertCatchingUp(Func<TestSpectatorPlayerClock> playerClock, bool catchingUp) =>
AddAssert($"player clock {playerClock().Id} {(catchingUp ? "is" : "is not")} catching up", () => playerClock().IsCatchingUp == catchingUp);
private void assertCatchingUp(Func<ISpectatorPlayerClock> playerClock, bool catchingUp) =>
AddAssert($"player clock {clocksById[playerClock()]} {(catchingUp ? "is" : "is not")} catching up", () => playerClock().IsCatchingUp == catchingUp);
private void assertPlayerClockState(Func<TestSpectatorPlayerClock> playerClock, bool running)
=> AddAssert($"player clock {playerClock().Id} {(running ? "is" : "is not")} running", () => playerClock().IsRunning == running);
private class TestSpectatorPlayerClock : TestManualClock, ISpectatorPlayerClock
{
public Bindable<bool> WaitingOnFrames { get; } = new Bindable<bool>(true);
public bool IsCatchingUp { get; set; }
public IFrameBasedClock Source
{
set => throw new NotImplementedException();
}
public readonly int Id;
public TestSpectatorPlayerClock(int id)
{
Id = id;
WaitingOnFrames.BindValueChanged(waiting =>
{
if (waiting.NewValue)
Stop();
else
Start();
});
}
public void ProcessFrame()
{
}
public double ElapsedFrameTime => 0;
public double FramesPerSecond => 0;
public FrameTimeInfo TimeInfo => default;
}
private void assertPlayerClockState(Func<ISpectatorPlayerClock> playerClock, bool running)
=> AddAssert($"player clock {clocksById[playerClock()]} {(running ? "is" : "is not")} running", () => playerClock().IsRunning == running);
private class TestManualClock : ManualClock, IAdjustableClock
{

View File

@ -17,15 +17,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// </summary>
public const double CATCHUP_RATE = 2;
/// <summary>
/// The source clock.
/// </summary>
public IFrameBasedClock? Source { get; set; }
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;
@ -67,9 +69,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
ElapsedFrameTime = 0;
FramesPerSecond = 0;
if (Source == null)
return;
Source.ProcessFrame();
if (IsRunning)

View File

@ -1,15 +1,12 @@
// 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.
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Timing;
using osu.Game.Screens.Play;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
@ -33,12 +30,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// </summary>
public const double MAXIMUM_START_DELAY = 15000;
public event Action ReadyToStart;
public event Action? ReadyToStart;
/// <summary>
/// The master clock which is used to control the timing of all player clocks clocks.
/// </summary>
public IAdjustableClock MasterClock { get; }
public GameplayClockContainer MasterClock { get; }
public IBindable<MasterClockState> MasterState => masterState;
@ -52,18 +49,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
private bool hasStarted;
private double? firstStartAttemptTime;
public CatchUpSyncManager(IAdjustableClock master)
public CatchUpSyncManager(GameplayClockContainer master)
{
MasterClock = master;
}
public void AddPlayerClock(ISpectatorPlayerClock clock)
public ISpectatorPlayerClock CreateManagedClock()
{
Debug.Assert(!playerClocks.Contains(clock));
var clock = new CatchUpSpectatorPlayerClock(MasterClock);
playerClocks.Add(clock);
return clock;
}
public void RemovePlayerClock(ISpectatorPlayerClock clock)
public void RemoveManagedClock(ISpectatorPlayerClock clock)
{
playerClocks.Remove(clock);
clock.Stop();

View File

@ -1,8 +1,6 @@
// 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.
#nullable disable
using osu.Framework.Bindables;
using osu.Framework.Timing;
@ -35,10 +33,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// Of note, this will be false if this clock is *ahead* of the master clock.
/// </remarks>
bool IsCatchingUp { get; set; }
/// <summary>
/// The source clock
/// </summary>
IFrameBasedClock Source { set; }
}
}

View File

@ -1,11 +1,9 @@
// 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.
#nullable disable
using System;
using osu.Framework.Bindables;
using osu.Framework.Timing;
using osu.Game.Screens.Play;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
@ -17,12 +15,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// <summary>
/// An event which is invoked when gameplay is ready to start.
/// </summary>
event Action ReadyToStart;
event Action? ReadyToStart;
/// <summary>
/// The master clock which player clocks should synchronise to.
/// </summary>
IAdjustableClock MasterClock { get; }
GameplayClockContainer MasterClock { get; }
/// <summary>
/// An event which is invoked when the state of <see cref="MasterClock"/> is changed.
@ -30,15 +28,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
IBindable<MasterClockState> MasterState { get; }
/// <summary>
/// Adds an <see cref="ISpectatorPlayerClock"/> to manage.
/// Create a new managed <see cref="ISpectatorPlayerClock"/>.
/// </summary>
/// <param name="clock">The <see cref="ISpectatorPlayerClock"/> to add.</param>
void AddPlayerClock(ISpectatorPlayerClock clock);
/// <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 RemovePlayerClock(ISpectatorPlayerClock clock);
void RemoveManagedClock(ISpectatorPlayerClock clock);
}
}

View File

@ -1,13 +1,11 @@
// 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.
#nullable disable
using System;
using System.Linq;
using JetBrains.Annotations;
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;
@ -42,17 +40,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
protected override UserActivity InitialActivity => new UserActivity.SpectatingMultiplayerGame(Beatmap.Value.BeatmapInfo, Ruleset.Value);
[Resolved]
private OsuColour colours { get; set; }
private OsuColour colours { get; set; } = null!;
[Resolved]
private MultiplayerClient multiplayerClient { get; set; }
private MultiplayerClient multiplayerClient { get; set; } = null!;
private readonly PlayerArea[] instances;
private MasterGameplayClockContainer masterClockContainer;
private ISyncManager syncManager;
private PlayerGrid grid;
private MultiSpectatorLeaderboard leaderboard;
private PlayerArea currentAudioSource;
private MasterGameplayClockContainer masterClockContainer = null!;
private ISyncManager syncManager = null!;
private PlayerGrid grid = null!;
private MultiSpectatorLeaderboard leaderboard = null!;
private PlayerArea? currentAudioSource;
private bool canStartMasterClock;
private readonly Room room;
@ -125,10 +123,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
};
for (int i = 0; i < Users.Count; i++)
{
grid.Add(instances[i] = new PlayerArea(Users[i], masterClockContainer));
syncManager.AddPlayerClock(instances[i].GameplayClock);
}
grid.Add(instances[i] = new PlayerArea(Users[i], syncManager.CreateManagedClock()));
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(users)
{
@ -181,7 +176,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
}
}
private bool isCandidateAudioSource([CanBeNull] ISpectatorPlayerClock clock)
private bool isCandidateAudioSource(ISpectatorPlayerClock? clock)
=> clock?.IsRunning == true && !clock.IsCatchingUp && !clock.WaitingOnFrames.Value;
private void onReadyToStart()
@ -189,7 +184,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
// 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.
double startTime = instances.Where(i => i.Score != null)
.SelectMany(i => i.Score.Replay.Frames)
.SelectMany(i => i.Score.AsNonNull().Replay.Frames)
.Select(f => f.Time)
.DefaultIfEmpty(0)
.Min();
@ -242,7 +237,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
var instance = instances.Single(i => i.UserId == userId);
instance.FadeColour(colours.Gray4, 400, Easing.OutQuint);
syncManager.RemovePlayerClock(instance.GameplayClock);
syncManager.RemoveManagedClock(instance.GameplayClock);
}
public override bool OnBackButton()

View File

@ -1,17 +1,13 @@
// 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.
#nullable disable
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
@ -29,7 +25,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// <summary>
/// Raised after <see cref="Player.StartGameplay"/> is called on <see cref="Player"/>.
/// </summary>
public event Action OnGameplayStarted;
public event Action? OnGameplayStarted;
/// <summary>
/// Whether a <see cref="Player"/> is loaded in the area.
@ -44,23 +40,25 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
/// <summary>
/// The <see cref="ISpectatorPlayerClock"/> used to control the gameplay running state of a loaded <see cref="Player"/>.
/// </summary>
[NotNull]
public readonly ISpectatorPlayerClock GameplayClock = new CatchUpSpectatorPlayerClock();
public readonly ISpectatorPlayerClock GameplayClock;
/// <summary>
/// The currently-loaded score.
/// </summary>
[CanBeNull]
public Score Score { get; private set; }
public Score? Score { get; private set; }
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
private readonly BindableDouble volumeAdjustment = new BindableDouble();
private readonly Container gameplayContent;
private readonly LoadingLayer loadingLayer;
private OsuScreenStack stack;
private OsuScreenStack? stack;
public PlayerArea(int userId, IFrameBasedClock masterClock)
public PlayerArea(int userId, ISpectatorPlayerClock clock)
{
UserId = userId;
GameplayClock = clock;
RelativeSizeAxes = Axes.Both;
Masking = true;
@ -77,14 +75,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
};
audioContainer.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment);
GameplayClock.Source = masterClock;
}
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; }
public void LoadScore([NotNull] Score score)
public void LoadScore(Score score)
{
if (Score != null)
throw new InvalidOperationException($"Cannot load a new score on a {nameof(PlayerArea)} that has an existing score.");