diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs index b84f7760e4..56cb6036c7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiSpectatorScreen.cs @@ -347,19 +347,44 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert($"{PLAYER_1_ID} score quit still set", () => getLeaderboardScore(PLAYER_1_ID).HasQuit.Value); } - private void loadSpectateScreen(bool waitForPlayerLoad = true) + /// + /// Tests spectating with a gameplay start time set to a negative value. + /// Simulating beatmaps with high or negative time storyboard elements. + /// + [Test] + public void TestNegativeGameplayStartTime() { - AddStep("load screen", () => + start(PLAYER_1_ID); + + loadSpectateScreen(false, -500); + + // to ensure negative gameplay start time does not affect spectator, send frames exactly after StartGameplay(). + // (similar to real spectating sessions in which the first frames get sent between StartGameplay() and player load complete) + AddStep("send frames at gameplay start", () => getInstance(PLAYER_1_ID).OnGameplayStarted += () => SpectatorClient.SendFrames(PLAYER_1_ID, 100)); + + AddUntilStep("wait for player load", () => spectatorScreen.AllPlayersLoaded); + + AddWaitStep("wait for progression", 3); + + assertNotCatchingUp(PLAYER_1_ID); + assertRunning(PLAYER_1_ID); + } + + private void loadSpectateScreen(bool waitForPlayerLoad = true, double? gameplayStartTime = null) + { + AddStep(!gameplayStartTime.HasValue ? "load screen" : $"load screen (start = {gameplayStartTime}ms)", () => { Beatmap.Value = beatmapManager.GetWorkingBeatmap(importedBeatmap); Ruleset.Value = importedBeatmap.Ruleset; - LoadScreen(spectatorScreen = new MultiSpectatorScreen(playingUsers.ToArray())); + LoadScreen(spectatorScreen = new TestMultiSpectatorScreen(playingUsers.ToArray(), gameplayStartTime)); }); AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectatorScreen.AllPlayersLoaded)); } + private void start(int userId, int? beatmapId = null) => start(new[] { userId }, beatmapId); + private void start(int[] userIds, int? beatmapId = null) { AddStep("start play", () => @@ -419,6 +444,12 @@ namespace osu.Game.Tests.Visual.Multiplayer private void assertMuted(int userId, bool muted) => AddAssert($"{userId} {(muted ? "is" : "is not")} muted", () => getInstance(userId).Mute == muted); + private void assertRunning(int userId) + => AddAssert($"{userId} clock running", () => getInstance(userId).GameplayClock.IsRunning); + + private void assertNotCatchingUp(int userId) + => AddAssert($"{userId} in sync", () => !getInstance(userId).GameplayClock.IsCatchingUp); + private void waitForCatchup(int userId) => AddUntilStep($"{userId} not catching up", () => !getInstance(userId).GameplayClock.IsCatchingUp); @@ -429,5 +460,19 @@ namespace osu.Game.Tests.Visual.Multiplayer private GameplayLeaderboardScore getLeaderboardScore(int userId) => spectatorScreen.ChildrenOfType().Single(s => s.User?.Id == userId); private int[] getPlayerIds(int count) => Enumerable.Range(PLAYER_1_ID, count).ToArray(); + + private class TestMultiSpectatorScreen : MultiSpectatorScreen + { + private readonly double? gameplayStartTime; + + public TestMultiSpectatorScreen(MultiplayerRoomUser[] users, double? gameplayStartTime = null) + : base(users) + { + this.gameplayStartTime = gameplayStartTime; + } + + protected override MasterGameplayClockContainer CreateMasterGameplayClockContainer(WorkingBeatmap beatmap) + => new MasterGameplayClockContainer(beatmap, gameplayStartTime ?? 0, gameplayStartTime.HasValue); + } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index 7350408eba..4646f42d63 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; 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.Spectator; @@ -68,7 +69,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate Container leaderboardContainer; Container scoreDisplayContainer; - masterClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0); + masterClockContainer = CreateMasterGameplayClockContainer(Beatmap.Value); InternalChildren = new[] { @@ -235,5 +236,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate return base.OnBackButton(); } + + protected virtual MasterGameplayClockContainer CreateMasterGameplayClockContainer(WorkingBeatmap beatmap) => new MasterGameplayClockContainer(beatmap, 0); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index 48f153ecbe..f5a6777a62 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -24,6 +24,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate /// public class PlayerArea : CompositeDrawable { + /// + /// Raised after is called on . + /// + public event Action OnGameplayStarted; + /// /// Whether a is loaded in the area. /// @@ -93,7 +98,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate } }; - stack.Push(new MultiSpectatorPlayerLoader(Score, () => new MultiSpectatorPlayer(Score, GameplayClock))); + stack.Push(new MultiSpectatorPlayerLoader(Score, () => + { + var player = new MultiSpectatorPlayer(Score, GameplayClock); + player.OnGameplayStarted += OnGameplayStarted; + return player; + })); + loadingLayer.Hide(); } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index cfca2d0a3d..0312789b12 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -45,6 +45,11 @@ namespace osu.Game.Screens.Play /// public const double RESULTS_DISPLAY_DELAY = 1000.0; + /// + /// Raised after is called. + /// + public event Action OnGameplayStarted; + public override bool AllowBackButton => false; // handled by HoldForMenuButton protected override UserActivity InitialActivity => new UserActivity.InSoloGame(Beatmap.Value.BeatmapInfo, Ruleset.Value); @@ -958,7 +963,9 @@ namespace osu.Game.Screens.Play updateGameplayState(); GameplayClockContainer.FadeInFromZero(750, Easing.OutQuint); + StartGameplay(); + OnGameplayStarted?.Invoke(); } ///