osu/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiplayerSpectator.cs

147 lines
4.6 KiB
C#
Raw Normal View History

// 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 System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Online.Rooms;
using osu.Game.Online.Spectator;
using osu.Game.Screens.Play;
using osu.Game.Screens.Spectate;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
public class MultiplayerSpectator : SpectatorScreen
{
private const double min_duration_to_allow_playback = 50;
private const double max_sync_offset = 2;
// Isolates beatmap/ruleset to this screen.
public override bool DisallowExternalBeatmapRulesetChanges => true;
public bool AllPlayersLoaded => instances.All(p => p?.PlayerLoaded == true);
[Resolved]
private SpectatorStreamingClient spectatorClient { get; set; }
private readonly int[] userIds;
private readonly PlayerInstance[] instances;
private PlayerGrid grid;
public MultiplayerSpectator(PlaylistItem playlistItem, int[] userIds)
: this(userIds.AsSpan().Slice(0, Math.Min(16, userIds.Length)).ToArray())
{
}
private MultiplayerSpectator(int[] userIds)
: base(userIds)
{
this.userIds = userIds;
instances = new PlayerInstance[userIds.Length];
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = grid = new PlayerGrid
{
RelativeSizeAxes = Axes.Both
};
}
protected override void Update()
{
base.Update();
updatePlayTime();
}
private bool gameplayStarted;
private void updatePlayTime()
{
if (gameplayStarted)
{
ensurePlaying(instances.Select(i => i.Beatmap.Track.CurrentTime).Max());
return;
}
// Make sure all players are loaded.
if (!AllPlayersLoaded)
{
ensureAllStopped();
return;
}
if (!instances.All(i => i.Score.Replay.Frames.Count > 0))
{
ensureAllStopped();
return;
}
gameplayStarted = true;
}
private void ensureAllStopped()
{
foreach (var inst in instances)
inst.ChildrenOfType<GameplayClockContainer>().SingleOrDefault()?.Stop();
}
private readonly BindableDouble catchupFrequencyAdjustment = new BindableDouble(2.0);
private void ensurePlaying(double targetTime)
{
foreach (var inst in instances)
{
double lastFrameTime = inst.Score.Replay.Frames.Select(f => f.Time).Last();
double currentTime = inst.Beatmap.Track.CurrentTime;
// If we have enough frames to play back, start playback.
if (Precision.DefinitelyBigger(lastFrameTime, currentTime, min_duration_to_allow_playback))
{
inst.ChildrenOfType<GameplayClockContainer>().Single().Start();
if (targetTime < lastFrameTime && targetTime > currentTime + 16)
inst.Beatmap.Track.AddAdjustment(AdjustableProperty.Frequency, catchupFrequencyAdjustment);
else
inst.Beatmap.Track.RemoveAdjustment(AdjustableProperty.Frequency, catchupFrequencyAdjustment);
}
else
inst.Beatmap.Track.RemoveAdjustment(AdjustableProperty.Frequency, catchupFrequencyAdjustment);
}
}
protected override void OnUserStateChanged(int userId, SpectatorState spectatorState)
{
}
protected override void StartGameplay(int userId, GameplayState gameplayState)
{
int userIndex = getIndexForUser(userId);
var existingInstance = instances[userIndex];
if (existingInstance != null)
grid.Remove(existingInstance);
LoadComponentAsync(instances[userIndex] = new PlayerInstance(gameplayState.Score), d =>
{
if (instances[userIndex] == d)
grid.Add(d);
});
}
protected override void EndGameplay(int userId)
{
spectatorClient.StopWatchingUser(userId);
}
private int getIndexForUser(int userId) => Array.IndexOf(userIds, userId);
}
}