mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 11:07:52 +08:00
Merge pull request #16827 from smoogipoo/spectator-state-rework
Add user state to SpectatorState, allowing multiplayer to continue to results
This commit is contained in:
commit
eda213e4de
@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
Player.ScoreProcessor.NewJudgement += b => judged = true;
|
||||
});
|
||||
AddUntilStep("swell judged", () => judged);
|
||||
AddAssert("failed", () => Player.HasFailed);
|
||||
AddAssert("failed", () => Player.GameplayState.HasFailed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
protected override void AddCheckSteps()
|
||||
{
|
||||
AddUntilStep("wait for fail", () => Player.HasFailed);
|
||||
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
|
||||
AddUntilStep("wait for fail overlay", () => ((FailPlayer)Player).FailOverlay.State.Value == Visibility.Visible);
|
||||
|
||||
// The pause screen and fail animation both ramp frequency.
|
||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
protected override void AddCheckSteps()
|
||||
{
|
||||
AddUntilStep("wait for fail", () => Player.HasFailed);
|
||||
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
|
||||
AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits > 1);
|
||||
AddAssert("total number of results == 1", () =>
|
||||
{
|
||||
|
@ -185,7 +185,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestPauseAfterFail()
|
||||
{
|
||||
AddUntilStep("wait for fail", () => Player.HasFailed);
|
||||
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
|
||||
AddUntilStep("fail overlay shown", () => Player.FailOverlayVisible);
|
||||
|
||||
confirmClockRunning(false);
|
||||
@ -201,7 +201,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestExitFromFailedGameplayAfterFailAnimation()
|
||||
{
|
||||
AddUntilStep("wait for fail", () => Player.HasFailed);
|
||||
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
|
||||
AddUntilStep("wait for fail overlay shown", () => Player.FailOverlayVisible);
|
||||
|
||||
confirmClockRunning(false);
|
||||
@ -213,7 +213,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestExitFromFailedGameplayDuringFailAnimation()
|
||||
{
|
||||
AddUntilStep("wait for fail", () => Player.HasFailed);
|
||||
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
|
||||
|
||||
// will finish the fail animation and show the fail/pause screen.
|
||||
AddStep("attempt exit via pause key", () => Player.ExitViaPause());
|
||||
@ -227,7 +227,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestQuickRetryFromFailedGameplay()
|
||||
{
|
||||
AddUntilStep("wait for fail", () => Player.HasFailed);
|
||||
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
|
||||
AddStep("quick retry", () => Player.GameplayClockContainer.ChildrenOfType<HotkeyRetryOverlay>().First().Action?.Invoke());
|
||||
|
||||
confirmExited();
|
||||
@ -236,7 +236,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestQuickExitFromFailedGameplay()
|
||||
{
|
||||
AddUntilStep("wait for fail", () => Player.HasFailed);
|
||||
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
|
||||
AddStep("quick exit", () => Player.GameplayClockContainer.ChildrenOfType<HotkeyExitOverlay>().First().Action?.Invoke());
|
||||
|
||||
confirmExited();
|
||||
@ -341,7 +341,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
confirmClockRunning(false);
|
||||
confirmNotExited();
|
||||
AddAssert("player not failed", () => !Player.HasFailed);
|
||||
AddAssert("player not failed", () => !Player.GameplayState.HasFailed);
|
||||
AddAssert("pause overlay shown", () => Player.PauseOverlayVisible);
|
||||
}
|
||||
|
||||
|
@ -159,7 +159,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
|
||||
|
||||
AddUntilStep("wait for fail", () => Player.HasFailed);
|
||||
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
|
||||
AddStep("exit", () => Player.Exit());
|
||||
|
||||
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
|
||||
@ -176,7 +176,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
addFakeHit();
|
||||
|
||||
AddUntilStep("wait for fail", () => Player.HasFailed);
|
||||
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
|
||||
AddStep("exit", () => Player.Exit());
|
||||
|
||||
AddAssert("ensure failing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == false);
|
||||
|
@ -155,11 +155,13 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
waitForPlayer();
|
||||
checkPaused(true);
|
||||
sendFrames();
|
||||
|
||||
finish();
|
||||
finish(SpectatedUserState.Failed);
|
||||
|
||||
checkPaused(false);
|
||||
// TODO: should replay until running out of frames then fail
|
||||
checkPaused(false); // Should continue playing until out of frames
|
||||
checkPaused(true); // And eventually stop after running out of frames and fail.
|
||||
// Todo: Should check for + display a failed message.
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -211,7 +213,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddStep("send frames and finish play", () =>
|
||||
{
|
||||
spectatorClient.HandleFrame(new OsuReplayFrame(1000, Vector2.Zero));
|
||||
spectatorClient.EndPlaying();
|
||||
spectatorClient.EndPlaying(new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset()) { HasPassed = true });
|
||||
});
|
||||
|
||||
// We can't access API because we're an "online" test.
|
||||
@ -234,6 +236,71 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddAssert("last frame has header", () => lastBundle.Frames[^1].Header != null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlayingState()
|
||||
{
|
||||
loadSpectatingScreen();
|
||||
|
||||
start();
|
||||
sendFrames();
|
||||
waitForPlayer();
|
||||
AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPassedState()
|
||||
{
|
||||
loadSpectatingScreen();
|
||||
|
||||
start();
|
||||
sendFrames();
|
||||
waitForPlayer();
|
||||
|
||||
AddStep("send passed", () => spectatorClient.EndPlay(streamingUser.Id, SpectatedUserState.Passed));
|
||||
AddUntilStep("state is passed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Passed);
|
||||
|
||||
start();
|
||||
sendFrames();
|
||||
waitForPlayer();
|
||||
AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestQuitState()
|
||||
{
|
||||
loadSpectatingScreen();
|
||||
|
||||
start();
|
||||
sendFrames();
|
||||
waitForPlayer();
|
||||
|
||||
AddStep("send quit", () => spectatorClient.EndPlay(streamingUser.Id));
|
||||
AddUntilStep("state is quit", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Quit);
|
||||
|
||||
start();
|
||||
sendFrames();
|
||||
waitForPlayer();
|
||||
AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFailedState()
|
||||
{
|
||||
loadSpectatingScreen();
|
||||
|
||||
start();
|
||||
sendFrames();
|
||||
waitForPlayer();
|
||||
|
||||
AddStep("send failed", () => spectatorClient.EndPlay(streamingUser.Id, SpectatedUserState.Failed));
|
||||
AddUntilStep("state is failed", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Failed);
|
||||
|
||||
start();
|
||||
sendFrames();
|
||||
waitForPlayer();
|
||||
AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing);
|
||||
}
|
||||
|
||||
private OsuFramedReplayInputHandler replayHandler =>
|
||||
(OsuFramedReplayInputHandler)Stack.ChildrenOfType<OsuInputManager>().First().ReplayInputHandler;
|
||||
|
||||
@ -246,7 +313,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
private void start(int? beatmapId = null) => AddStep("start play", () => spectatorClient.StartPlay(streamingUser.Id, beatmapId ?? importedBeatmapId));
|
||||
|
||||
private void finish() => AddStep("end play", () => spectatorClient.EndPlay(streamingUser.Id));
|
||||
private void finish(SpectatedUserState state = SpectatedUserState.Quit) => AddStep("end play", () => spectatorClient.EndPlay(streamingUser.Id, state));
|
||||
|
||||
private void checkPaused(bool state) =>
|
||||
AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType<DrawableRuleset>().First().IsPaused.Value == state);
|
||||
|
@ -37,8 +37,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestClientSendsCorrectRuleset()
|
||||
{
|
||||
AddUntilStep("spectator client sending frames", () => spectatorClient.PlayingUserStates.ContainsKey(dummy_user_id));
|
||||
AddAssert("spectator client sent correct ruleset", () => spectatorClient.PlayingUserStates[dummy_user_id].RulesetID == Ruleset.Value.OnlineID);
|
||||
AddUntilStep("spectator client sending frames", () => spectatorClient.WatchedUserStates.ContainsKey(dummy_user_id));
|
||||
AddAssert("spectator client sent correct ruleset", () => spectatorClient.WatchedUserStates[dummy_user_id].RulesetID == Ruleset.Value.OnlineID);
|
||||
}
|
||||
|
||||
public override void TearDownSteps()
|
||||
|
@ -96,7 +96,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddStep("set storyboard duration to 0.6s", () => currentStoryboardDuration = 600);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for fail", () => Player.HasFailed);
|
||||
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
|
||||
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
|
||||
AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible);
|
||||
}
|
||||
|
@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
public void RandomlyUpdateState()
|
||||
{
|
||||
foreach (int userId in PlayingUsers)
|
||||
foreach ((int userId, _) in WatchedUserStates)
|
||||
{
|
||||
if (RNG.NextBool())
|
||||
continue;
|
||||
|
@ -211,7 +211,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
return (player = Game.ScreenStack.CurrentScreen as Player) != null;
|
||||
});
|
||||
|
||||
AddUntilStep("wait for fail", () => player.HasFailed);
|
||||
AddUntilStep("wait for fail", () => player.GameplayState.HasFailed);
|
||||
|
||||
AddUntilStep("wait for track stop", () => !Game.MusicController.IsPlaying);
|
||||
AddAssert("Ensure time before preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().BeatmapInfo.Metadata.PreviewTime);
|
||||
|
38
osu.Game/Online/Spectator/SpectatedUserState.cs
Normal file
38
osu.Game/Online/Spectator/SpectatedUserState.cs
Normal file
@ -0,0 +1,38 @@
|
||||
// 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.
|
||||
|
||||
namespace osu.Game.Online.Spectator
|
||||
{
|
||||
public enum SpectatedUserState
|
||||
{
|
||||
/// <summary>
|
||||
/// The spectated user is not yet playing.
|
||||
/// </summary>
|
||||
Idle,
|
||||
|
||||
/// <summary>
|
||||
/// The spectated user is currently playing.
|
||||
/// </summary>
|
||||
Playing,
|
||||
|
||||
/// <summary>
|
||||
/// The spectated user is currently paused. Unused for the time being.
|
||||
/// </summary>
|
||||
Paused,
|
||||
|
||||
/// <summary>
|
||||
/// The spectated user has passed gameplay.
|
||||
/// </summary>
|
||||
Passed,
|
||||
|
||||
/// <summary>
|
||||
/// The spectated user has failed gameplay.
|
||||
/// </summary>
|
||||
Failed,
|
||||
|
||||
/// <summary>
|
||||
/// The spectated user has quit gameplay.
|
||||
/// </summary>
|
||||
Quit
|
||||
}
|
||||
}
|
@ -35,19 +35,28 @@ namespace osu.Game.Online.Spectator
|
||||
/// </summary>
|
||||
public abstract IBindable<bool> IsConnected { get; }
|
||||
|
||||
private readonly List<int> watchingUsers = new List<int>();
|
||||
/// <summary>
|
||||
/// The states of all users currently being watched.
|
||||
/// </summary>
|
||||
public IBindableDictionary<int, SpectatorState> WatchedUserStates => watchedUserStates;
|
||||
|
||||
/// <summary>
|
||||
/// A global list of all players currently playing.
|
||||
/// </summary>
|
||||
public IBindableList<int> PlayingUsers => playingUsers;
|
||||
private readonly BindableList<int> playingUsers = new BindableList<int>();
|
||||
|
||||
public IBindableDictionary<int, SpectatorState> PlayingUserStates => playingUserStates;
|
||||
private readonly BindableDictionary<int, SpectatorState> playingUserStates = new BindableDictionary<int, SpectatorState>();
|
||||
/// <summary>
|
||||
/// All users currently being watched.
|
||||
/// </summary>
|
||||
private readonly List<int> watchedUsers = new List<int>();
|
||||
|
||||
private readonly BindableDictionary<int, SpectatorState> watchedUserStates = new BindableDictionary<int, SpectatorState>();
|
||||
private readonly BindableList<int> playingUsers = new BindableList<int>();
|
||||
private readonly SpectatorState currentState = new SpectatorState();
|
||||
|
||||
private IBeatmap? currentBeatmap;
|
||||
private Score? currentScore;
|
||||
|
||||
private readonly SpectatorState currentState = new SpectatorState();
|
||||
|
||||
/// <summary>
|
||||
/// Whether the local user is playing.
|
||||
/// </summary>
|
||||
@ -76,8 +85,8 @@ namespace osu.Game.Online.Spectator
|
||||
if (connected.NewValue)
|
||||
{
|
||||
// get all the users that were previously being watched
|
||||
int[] users = watchingUsers.ToArray();
|
||||
watchingUsers.Clear();
|
||||
int[] users = watchedUsers.ToArray();
|
||||
watchedUsers.Clear();
|
||||
|
||||
// resubscribe to watched users.
|
||||
foreach (int userId in users)
|
||||
@ -90,7 +99,7 @@ namespace osu.Game.Online.Spectator
|
||||
else
|
||||
{
|
||||
playingUsers.Clear();
|
||||
playingUserStates.Clear();
|
||||
watchedUserStates.Clear();
|
||||
}
|
||||
}), true);
|
||||
}
|
||||
@ -102,11 +111,8 @@ namespace osu.Game.Online.Spectator
|
||||
if (!playingUsers.Contains(userId))
|
||||
playingUsers.Add(userId);
|
||||
|
||||
// UserBeganPlaying() is called by the server regardless of whether the local user is watching the remote user, and is called a further time when the remote user is watched.
|
||||
// This may be a temporary thing (see: https://github.com/ppy/osu-server-spectator/blob/2273778e02cfdb4a9c6a934f2a46a8459cb5d29c/osu.Server.Spectator/Hubs/SpectatorHub.cs#L28-L29).
|
||||
// We don't want the user states to update unless the player is being watched, otherwise calling BindUserBeganPlaying() can lead to double invocations.
|
||||
if (watchingUsers.Contains(userId))
|
||||
playingUserStates[userId] = state;
|
||||
if (watchedUsers.Contains(userId))
|
||||
watchedUserStates[userId] = state;
|
||||
|
||||
OnUserBeganPlaying?.Invoke(userId, state);
|
||||
});
|
||||
@ -119,7 +125,9 @@ namespace osu.Game.Online.Spectator
|
||||
Schedule(() =>
|
||||
{
|
||||
playingUsers.Remove(userId);
|
||||
playingUserStates.Remove(userId);
|
||||
|
||||
if (watchedUsers.Contains(userId))
|
||||
watchedUserStates[userId] = state;
|
||||
|
||||
OnUserFinishedPlaying?.Invoke(userId, state);
|
||||
});
|
||||
@ -151,6 +159,7 @@ namespace osu.Game.Online.Spectator
|
||||
currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineID;
|
||||
currentState.RulesetID = score.ScoreInfo.RulesetID;
|
||||
currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray();
|
||||
currentState.State = SpectatedUserState.Playing;
|
||||
|
||||
currentBeatmap = state.Beatmap;
|
||||
currentScore = score;
|
||||
@ -161,7 +170,7 @@ namespace osu.Game.Online.Spectator
|
||||
|
||||
public void SendFrames(FrameDataBundle data) => lastSend = SendFramesInternal(data);
|
||||
|
||||
public void EndPlaying()
|
||||
public void EndPlaying(GameplayState state)
|
||||
{
|
||||
// This method is most commonly called via Dispose(), which is can be asynchronous (via the AsyncDisposalQueue).
|
||||
// We probably need to find a better way to handle this...
|
||||
@ -176,6 +185,13 @@ namespace osu.Game.Online.Spectator
|
||||
IsPlaying = false;
|
||||
currentBeatmap = null;
|
||||
|
||||
if (state.HasPassed)
|
||||
currentState.State = SpectatedUserState.Passed;
|
||||
else if (state.HasFailed)
|
||||
currentState.State = SpectatedUserState.Failed;
|
||||
else
|
||||
currentState.State = SpectatedUserState.Quit;
|
||||
|
||||
EndPlayingInternal(currentState);
|
||||
});
|
||||
}
|
||||
@ -184,10 +200,10 @@ namespace osu.Game.Online.Spectator
|
||||
{
|
||||
Debug.Assert(ThreadSafety.IsUpdateThread);
|
||||
|
||||
if (watchingUsers.Contains(userId))
|
||||
if (watchedUsers.Contains(userId))
|
||||
return;
|
||||
|
||||
watchingUsers.Add(userId);
|
||||
watchedUsers.Add(userId);
|
||||
|
||||
WatchUserInternal(userId);
|
||||
}
|
||||
@ -198,8 +214,8 @@ namespace osu.Game.Online.Spectator
|
||||
// Todo: This should not be a thing, but requires framework changes.
|
||||
Schedule(() =>
|
||||
{
|
||||
watchingUsers.Remove(userId);
|
||||
playingUserStates.Remove(userId);
|
||||
watchedUsers.Remove(userId);
|
||||
watchedUserStates.Remove(userId);
|
||||
StopWatchingUserInternal(userId);
|
||||
});
|
||||
}
|
||||
|
@ -24,14 +24,17 @@ namespace osu.Game.Online.Spectator
|
||||
[Key(2)]
|
||||
public IEnumerable<APIMod> Mods { get; set; } = Enumerable.Empty<APIMod>();
|
||||
|
||||
[Key(3)]
|
||||
public SpectatedUserState State { get; set; }
|
||||
|
||||
public bool Equals(SpectatorState other)
|
||||
{
|
||||
if (ReferenceEquals(null, other)) return false;
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
|
||||
return BeatmapID == other.BeatmapID && Mods.SequenceEqual(other.Mods) && RulesetID == other.RulesetID;
|
||||
return BeatmapID == other.BeatmapID && Mods.SequenceEqual(other.Mods) && RulesetID == other.RulesetID && State == other.State;
|
||||
}
|
||||
|
||||
public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)} Ruleset:{RulesetID}";
|
||||
public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)} Ruleset:{RulesetID} State:{State}";
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@ -52,21 +53,24 @@ namespace osu.Game.Overlays.Dashboard
|
||||
base.LoadComplete();
|
||||
|
||||
playingUsers.BindTo(spectatorClient.PlayingUsers);
|
||||
playingUsers.BindCollectionChanged(onUsersChanged, true);
|
||||
playingUsers.BindCollectionChanged(onPlayingUsersChanged, true);
|
||||
}
|
||||
|
||||
private void onUsersChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() =>
|
||||
private void onPlayingUsersChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() =>
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
foreach (int id in e.NewItems.OfType<int>().ToArray())
|
||||
Debug.Assert(e.NewItems != null);
|
||||
|
||||
foreach (int userId in e.NewItems)
|
||||
{
|
||||
users.GetUserAsync(id).ContinueWith(task =>
|
||||
users.GetUserAsync(userId).ContinueWith(task =>
|
||||
{
|
||||
var user = task.GetResultSafely();
|
||||
|
||||
if (user == null) return;
|
||||
if (user == null)
|
||||
return;
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
@ -82,12 +86,10 @@ namespace osu.Game.Overlays.Dashboard
|
||||
break;
|
||||
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
foreach (int u in e.OldItems.OfType<int>())
|
||||
userFlow.FirstOrDefault(card => card.User.Id == u)?.Expire();
|
||||
break;
|
||||
Debug.Assert(e.OldItems != null);
|
||||
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
userFlow.Clear();
|
||||
foreach (int userId in e.OldItems)
|
||||
userFlow.FirstOrDefault(card => card.User.Id == userId)?.Expire();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
public int RecordFrameRate = 60;
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
[Resolved]
|
||||
private SpectatorClient spectatorClient { get; set; }
|
||||
|
||||
[Resolved]
|
||||
@ -48,14 +48,13 @@ namespace osu.Game.Rulesets.UI
|
||||
base.LoadComplete();
|
||||
|
||||
inputManager = GetContainingInputManager();
|
||||
|
||||
spectatorClient?.BeginPlaying(gameplayState, target);
|
||||
spectatorClient.BeginPlaying(gameplayState, target);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
spectatorClient?.EndPlaying();
|
||||
spectatorClient?.EndPlaying(gameplayState);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
|
@ -9,7 +9,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
{
|
||||
public class MultiplayerPlayerLoader : PlayerLoader
|
||||
{
|
||||
public bool GameplayPassed => player?.GameplayPassed == true;
|
||||
public bool GameplayPassed => player?.GameplayState.HasPassed == true;
|
||||
|
||||
private Player player;
|
||||
|
||||
|
@ -207,15 +207,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnUserStateChanged(int userId, SpectatorState spectatorState)
|
||||
protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState)
|
||||
=> instances.Single(i => i.UserId == userId).LoadScore(spectatorGameplayState.Score);
|
||||
|
||||
protected override void EndGameplay(int userId)
|
||||
protected override void EndGameplay(int userId, SpectatorState state)
|
||||
{
|
||||
// Allowed passed/failed users to complete their remaining replay frames.
|
||||
// The failed state isn't really possible in multiplayer (yet?) but is added here just for safety in case it starts being used.
|
||||
if (state.State == SpectatedUserState.Passed || state.State == SpectatedUserState.Failed)
|
||||
return;
|
||||
|
||||
RemoveUser(userId);
|
||||
|
||||
var instance = instances.Single(i => i.UserId == userId);
|
||||
|
@ -39,6 +39,21 @@ namespace osu.Game.Screens.Play
|
||||
/// </summary>
|
||||
public readonly Score Score;
|
||||
|
||||
/// <summary>
|
||||
/// Whether gameplay completed without the user failing.
|
||||
/// </summary>
|
||||
public bool HasPassed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the user failed during gameplay.
|
||||
/// </summary>
|
||||
public bool HasFailed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the user quit gameplay without having either passed or failed.
|
||||
/// </summary>
|
||||
public bool HasQuit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A bindable tracking the last judgement result applied to any hit object.
|
||||
/// </summary>
|
||||
|
@ -72,15 +72,8 @@ namespace osu.Game.Screens.Play
|
||||
/// </summary>
|
||||
protected virtual bool PauseOnFocusLost => true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether gameplay has completed without the user having failed.
|
||||
/// </summary>
|
||||
public bool GameplayPassed { get; private set; }
|
||||
|
||||
public Action RestartRequested;
|
||||
|
||||
public bool HasFailed { get; private set; }
|
||||
|
||||
private Bindable<bool> mouseWheelDisabled;
|
||||
|
||||
private readonly Bindable<bool> storyboardReplacesBackground = new Bindable<bool>();
|
||||
@ -560,7 +553,7 @@ namespace osu.Game.Screens.Play
|
||||
if (showDialogFirst && !pauseOrFailDialogVisible)
|
||||
{
|
||||
// if the fail animation is currently in progress, accelerate it (it will show the pause dialog on completion).
|
||||
if (ValidForResume && HasFailed)
|
||||
if (ValidForResume && GameplayState.HasFailed)
|
||||
{
|
||||
failAnimationLayer.FinishTransforms(true);
|
||||
return;
|
||||
@ -679,7 +672,7 @@ namespace osu.Game.Screens.Play
|
||||
resultsDisplayDelegate?.Cancel();
|
||||
resultsDisplayDelegate = null;
|
||||
|
||||
GameplayPassed = false;
|
||||
GameplayState.HasPassed = false;
|
||||
ValidForResume = true;
|
||||
skipOutroOverlay.Hide();
|
||||
return;
|
||||
@ -689,7 +682,7 @@ namespace osu.Game.Screens.Play
|
||||
if (HealthProcessor.HasFailed)
|
||||
return;
|
||||
|
||||
GameplayPassed = true;
|
||||
GameplayState.HasPassed = true;
|
||||
|
||||
// Setting this early in the process means that even if something were to go wrong in the order of events following, there
|
||||
// is no chance that a user could return to the (already completed) Player instance from a child screen.
|
||||
@ -805,7 +798,7 @@ namespace osu.Game.Screens.Play
|
||||
if (!CheckModsAllowFailure())
|
||||
return false;
|
||||
|
||||
HasFailed = true;
|
||||
GameplayState.HasFailed = true;
|
||||
Score.ScoreInfo.Passed = false;
|
||||
|
||||
// There is a chance that we could be in a paused state as the ruleset's internal clock (see FrameStabilityContainer)
|
||||
@ -860,13 +853,13 @@ namespace osu.Game.Screens.Play
|
||||
// replays cannot be paused and exit immediately
|
||||
&& !DrawableRuleset.HasReplayLoaded.Value
|
||||
// cannot pause if we are already in a fail state
|
||||
&& !HasFailed;
|
||||
&& !GameplayState.HasFailed;
|
||||
|
||||
private bool canResume =>
|
||||
// cannot resume from a non-paused state
|
||||
GameplayClockContainer.IsPaused.Value
|
||||
// cannot resume if we are already in a fail state
|
||||
&& !HasFailed
|
||||
&& !GameplayState.HasFailed
|
||||
// already resuming
|
||||
&& !IsResuming;
|
||||
|
||||
@ -991,6 +984,9 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
public override bool OnExiting(IScreen next)
|
||||
{
|
||||
if (!GameplayState.HasPassed && !GameplayState.HasFailed)
|
||||
GameplayState.HasQuit = true;
|
||||
|
||||
screenSuspension?.RemoveAndDisposeImmediately();
|
||||
failAnimationLayer?.RemoveFilters();
|
||||
|
||||
@ -1005,7 +1001,7 @@ namespace osu.Game.Screens.Play
|
||||
// EndPlaying() is typically called from ReplayRecorder.Dispose(). Disposal is currently asynchronous.
|
||||
// To resolve test failures, forcefully end playing synchronously when this screen exits.
|
||||
// Todo: Replace this with a more permanent solution once osu-framework has a synchronous cleanup method.
|
||||
spectatorClient.EndPlaying();
|
||||
spectatorClient.EndPlaying(GameplayState);
|
||||
|
||||
// GameplayClockContainer performs seeks / start / stop operations on the beatmap's track.
|
||||
// as we are no longer the current screen, we cannot guarantee the track is still usable.
|
||||
|
@ -166,7 +166,7 @@ namespace osu.Game.Screens.Play
|
||||
automaticDownload.Current.BindValueChanged(_ => checkForAutomaticDownload());
|
||||
}
|
||||
|
||||
protected override void OnUserStateChanged(int userId, SpectatorState spectatorState)
|
||||
protected override void OnNewPlayingUserState(int userId, SpectatorState spectatorState)
|
||||
{
|
||||
clearDisplay();
|
||||
showBeatmapPanel(spectatorState);
|
||||
@ -180,7 +180,7 @@ namespace osu.Game.Screens.Play
|
||||
scheduleStart(spectatorGameplayState);
|
||||
}
|
||||
|
||||
protected override void EndGameplay(int userId)
|
||||
protected override void EndGameplay(int userId, SpectatorState state)
|
||||
{
|
||||
scheduledStart?.Cancel();
|
||||
immediateSpectatorGameplayState = null;
|
||||
|
@ -42,7 +42,7 @@ namespace osu.Game.Screens.Spectate
|
||||
[Resolved]
|
||||
private UserLookupCache userLookupCache { get; set; }
|
||||
|
||||
private readonly IBindableDictionary<int, SpectatorState> playingUserStates = new BindableDictionary<int, SpectatorState>();
|
||||
private readonly IBindableDictionary<int, SpectatorState> userStates = new BindableDictionary<int, SpectatorState>();
|
||||
|
||||
private readonly Dictionary<int, APIUser> userMap = new Dictionary<int, APIUser>();
|
||||
private readonly Dictionary<int, SpectatorGameplayState> gameplayStates = new Dictionary<int, SpectatorGameplayState>();
|
||||
@ -77,8 +77,8 @@ namespace osu.Game.Screens.Spectate
|
||||
userMap[u.Id] = u;
|
||||
}
|
||||
|
||||
playingUserStates.BindTo(spectatorClient.PlayingUserStates);
|
||||
playingUserStates.BindCollectionChanged(onPlayingUserStatesChanged, true);
|
||||
userStates.BindTo(spectatorClient.WatchedUserStates);
|
||||
userStates.BindCollectionChanged(onUserStatesChanged, true);
|
||||
|
||||
realmSubscription = realm.RegisterForNotifications(
|
||||
realm => realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending), beatmapsChanged);
|
||||
@ -99,51 +99,55 @@ namespace osu.Game.Screens.Spectate
|
||||
{
|
||||
foreach ((int userId, _) in userMap)
|
||||
{
|
||||
if (!playingUserStates.TryGetValue(userId, out var userState))
|
||||
if (!userStates.TryGetValue(userId, out var userState))
|
||||
continue;
|
||||
|
||||
if (beatmapSet.Beatmaps.Any(b => b.OnlineID == userState.BeatmapID))
|
||||
updateGameplayState(userId);
|
||||
startGameplay(userId);
|
||||
}
|
||||
}
|
||||
|
||||
private void onPlayingUserStatesChanged(object sender, NotifyDictionaryChangedEventArgs<int, SpectatorState> e)
|
||||
private void onUserStatesChanged(object sender, NotifyDictionaryChangedEventArgs<int, SpectatorState> e)
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyDictionaryChangedAction.Add:
|
||||
case NotifyDictionaryChangedAction.Replace:
|
||||
foreach ((int userId, var state) in e.NewItems.AsNonNull())
|
||||
onUserStateAdded(userId, state);
|
||||
onUserStateChanged(userId, state);
|
||||
break;
|
||||
|
||||
case NotifyDictionaryChangedAction.Remove:
|
||||
foreach ((int userId, var _) in e.OldItems.AsNonNull())
|
||||
onUserStateRemoved(userId);
|
||||
break;
|
||||
|
||||
case NotifyDictionaryChangedAction.Replace:
|
||||
foreach ((int userId, var _) in e.OldItems.AsNonNull())
|
||||
onUserStateRemoved(userId);
|
||||
|
||||
foreach ((int userId, var state) in e.NewItems.AsNonNull())
|
||||
onUserStateAdded(userId, state);
|
||||
foreach ((int userId, SpectatorState state) in e.OldItems.AsNonNull())
|
||||
onUserStateRemoved(userId, state);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void onUserStateAdded(int userId, SpectatorState state)
|
||||
private void onUserStateChanged(int userId, SpectatorState newState)
|
||||
{
|
||||
if (state.RulesetID == null || state.BeatmapID == null)
|
||||
if (newState.RulesetID == null || newState.BeatmapID == null)
|
||||
return;
|
||||
|
||||
if (!userMap.ContainsKey(userId))
|
||||
return;
|
||||
|
||||
Schedule(() => OnUserStateChanged(userId, state));
|
||||
updateGameplayState(userId);
|
||||
switch (newState.State)
|
||||
{
|
||||
case SpectatedUserState.Passed:
|
||||
// Make sure that gameplay completes to the end.
|
||||
if (gameplayStates.TryGetValue(userId, out var gameplayState))
|
||||
gameplayState.Score.Replay.HasReceivedAllFrames = true;
|
||||
break;
|
||||
|
||||
case SpectatedUserState.Playing:
|
||||
Schedule(() => OnNewPlayingUserState(userId, newState));
|
||||
startGameplay(userId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void onUserStateRemoved(int userId)
|
||||
private void onUserStateRemoved(int userId, SpectatorState state)
|
||||
{
|
||||
if (!userMap.ContainsKey(userId))
|
||||
return;
|
||||
@ -154,15 +158,15 @@ namespace osu.Game.Screens.Spectate
|
||||
gameplayState.Score.Replay.HasReceivedAllFrames = true;
|
||||
|
||||
gameplayStates.Remove(userId);
|
||||
Schedule(() => EndGameplay(userId));
|
||||
Schedule(() => EndGameplay(userId, state));
|
||||
}
|
||||
|
||||
private void updateGameplayState(int userId)
|
||||
private void startGameplay(int userId)
|
||||
{
|
||||
Debug.Assert(userMap.ContainsKey(userId));
|
||||
|
||||
var user = userMap[userId];
|
||||
var spectatorState = playingUserStates[userId];
|
||||
var spectatorState = userStates[userId];
|
||||
|
||||
var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.OnlineID == spectatorState.RulesetID)?.CreateInstance();
|
||||
if (resolvedRuleset == null)
|
||||
@ -191,11 +195,11 @@ namespace osu.Game.Screens.Spectate
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a spectated user's state has changed.
|
||||
/// Invoked when a spectated user's state has changed to a new state indicating the player is currently playing.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user whose state has changed.</param>
|
||||
/// <param name="spectatorState">The new state.</param>
|
||||
protected abstract void OnUserStateChanged(int userId, [NotNull] SpectatorState spectatorState);
|
||||
protected abstract void OnNewPlayingUserState(int userId, [NotNull] SpectatorState spectatorState);
|
||||
|
||||
/// <summary>
|
||||
/// Starts gameplay for a user.
|
||||
@ -208,7 +212,8 @@ namespace osu.Game.Screens.Spectate
|
||||
/// Ends gameplay for a user.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user to end gameplay for.</param>
|
||||
protected abstract void EndGameplay(int userId);
|
||||
/// <param name="state">The final user state.</param>
|
||||
protected abstract void EndGameplay(int userId, SpectatorState state);
|
||||
|
||||
/// <summary>
|
||||
/// Stops spectating a user.
|
||||
@ -216,7 +221,10 @@ namespace osu.Game.Screens.Spectate
|
||||
/// <param name="userId">The user to stop spectating.</param>
|
||||
protected void RemoveUser(int userId)
|
||||
{
|
||||
onUserStateRemoved(userId);
|
||||
if (!userStates.TryGetValue(userId, out var state))
|
||||
return;
|
||||
|
||||
onUserStateRemoved(userId, state);
|
||||
|
||||
users.Remove(userId);
|
||||
userMap.Remove(userId);
|
||||
|
@ -58,7 +58,8 @@ namespace osu.Game.Tests.Visual.Spectator
|
||||
/// Ends play for an arbitrary user.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user to end play for.</param>
|
||||
public void EndPlay(int userId)
|
||||
/// <param name="state">The spectator state to end play with.</param>
|
||||
public void EndPlay(int userId, SpectatedUserState state = SpectatedUserState.Quit)
|
||||
{
|
||||
if (!userBeatmapDictionary.ContainsKey(userId))
|
||||
return;
|
||||
@ -67,6 +68,7 @@ namespace osu.Game.Tests.Visual.Spectator
|
||||
{
|
||||
BeatmapID = userBeatmapDictionary[userId],
|
||||
RulesetID = 0,
|
||||
State = state
|
||||
});
|
||||
|
||||
userBeatmapDictionary.Remove(userId);
|
||||
@ -142,6 +144,7 @@ namespace osu.Game.Tests.Visual.Spectator
|
||||
{
|
||||
BeatmapID = userBeatmapDictionary[userId],
|
||||
RulesetID = 0,
|
||||
State = SpectatedUserState.Playing
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user