1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 16:52:54 +08:00

Merge pull request #10611 from peppy/spectator-listing

Add "currently playing" player listing and spectator UI flow
This commit is contained in:
Dan Balasescu 2020-11-04 14:08:11 +09:00 committed by GitHub
commit c6779b414e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 465 additions and 59 deletions

View File

@ -3,8 +3,10 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Screens;
using osu.Framework.Testing;
@ -227,12 +229,19 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded);
}
internal class TestSpectatorStreamingClient : SpectatorStreamingClient
public class TestSpectatorStreamingClient : SpectatorStreamingClient
{
public readonly User StreamingUser = new User { Id = 1234, Username = "Test user" };
public new BindableList<int> PlayingUsers => (BindableList<int>)base.PlayingUsers;
private int beatmapId;
protected override Task Connect()
{
return Task.CompletedTask;
}
public void StartPlay(int beatmapId)
{
this.beatmapId = beatmapId;

View File

@ -0,0 +1,66 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Spectator;
using osu.Game.Overlays.Dashboard;
using osu.Game.Tests.Visual.Gameplay;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneCurrentlyPlayingDisplay : OsuTestScene
{
[Cached(typeof(SpectatorStreamingClient))]
private TestSceneSpectator.TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSceneSpectator.TestSpectatorStreamingClient();
private CurrentlyPlayingDisplay currentlyPlaying;
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("register request handling", () => dummyAPI.HandleRequest = req =>
{
switch (req)
{
case GetUserRequest cRequest:
cRequest.TriggerSuccess(new User { Username = "peppy", Id = 2 });
break;
}
});
AddStep("add streaming client", () =>
{
Remove(testSpectatorStreamingClient);
Children = new Drawable[]
{
testSpectatorStreamingClient,
currentlyPlaying = new CurrentlyPlayingDisplay
{
RelativeSizeAxes = Axes.Both,
}
};
});
AddStep("Reset players", () => testSpectatorStreamingClient.PlayingUsers.Clear());
}
[Test]
public void TestBasicDisplay()
{
AddStep("Add playing user", () => testSpectatorStreamingClient.PlayingUsers.Add(2));
AddUntilStep("Panel loaded", () => currentlyPlaying.ChildrenOfType<UserGridPanel>()?.FirstOrDefault()?.User.Id == 2);
AddStep("Remove playing user", () => testSpectatorStreamingClient.PlayingUsers.Remove(2));
AddUntilStep("Panel no longer present", () => !currentlyPlaying.ChildrenOfType<UserGridPanel>().Any());
}
}
}

View File

@ -42,6 +42,8 @@ namespace osu.Game.Configuration
Set(OsuSetting.Username, string.Empty);
Set(OsuSetting.Token, string.Empty);
Set(OsuSetting.AutomaticallyDownloadWhenSpectating, false);
Set(OsuSetting.SavePassword, false).ValueChanged += enabled =>
{
if (enabled.NewValue) Set(OsuSetting.SaveUsername, true);
@ -245,5 +247,6 @@ namespace osu.Game.Configuration
GameplayDisableWinKey,
SeasonalBackgroundMode,
EditorWaveformOpacity,
AutomaticallyDownloadWhenSpectating,
}
}

View File

@ -93,14 +93,14 @@ namespace osu.Game.Online.Spectator
break;
case APIState.Online:
Task.Run(connect);
Task.Run(Connect);
break;
}
}
private const string endpoint = "https://spectator.ppy.sh/spectator";
private async Task connect()
protected virtual async Task Connect()
{
if (connection != null)
return;

View File

@ -0,0 +1,132 @@
// 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.Collections.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Spectator;
using osu.Game.Screens.Multi.Match.Components;
using osu.Game.Screens.Play;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Overlays.Dashboard
{
internal class CurrentlyPlayingDisplay : CompositeDrawable
{
private IBindableList<int> playingUsers;
private FillFlowContainer<PlayingUserPanel> userFlow;
[Resolved]
private SpectatorStreamingClient spectatorStreaming { get; set; }
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = userFlow = new FillFlowContainer<PlayingUserPanel>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(10),
};
}
[Resolved]
private IAPIProvider api { get; set; }
protected override void LoadComplete()
{
base.LoadComplete();
playingUsers = spectatorStreaming.PlayingUsers.GetBoundCopy();
playingUsers.BindCollectionChanged((sender, e) => Schedule(() =>
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (var u in e.NewItems.OfType<int>())
{
var request = new GetUserRequest(u);
request.Success += user => Schedule(() =>
{
if (playingUsers.Contains((int)user.Id))
userFlow.Add(createUserPanel(user));
});
api.Queue(request);
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (var u in e.OldItems.OfType<int>())
userFlow.FirstOrDefault(card => card.User.Id == u)?.Expire();
break;
case NotifyCollectionChangedAction.Reset:
userFlow.Clear();
break;
}
}), true);
}
private PlayingUserPanel createUserPanel(User user) =>
new PlayingUserPanel(user).With(panel =>
{
panel.Anchor = Anchor.TopCentre;
panel.Origin = Anchor.TopCentre;
});
private class PlayingUserPanel : CompositeDrawable
{
public readonly User User;
[Resolved(canBeNull: true)]
private OsuGame game { get; set; }
public PlayingUserPanel(User user)
{
User = user;
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(2),
Width = 290,
Children = new Drawable[]
{
new UserGridPanel(user)
{
RelativeSizeAxes = Axes.X,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
new PurpleTriangleButton
{
RelativeSizeAxes = Axes.X,
Text = "Watch",
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Action = () => game?.PerformFromScreen(s => s.Push(new Spectator(user)))
}
}
},
};
}
}
}
}

View File

@ -1,6 +1,8 @@
// 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.ComponentModel;
namespace osu.Game.Overlays.Dashboard
{
public class DashboardOverlayHeader : TabControlOverlayHeader<DashboardOverlayTabs>
@ -20,6 +22,9 @@ namespace osu.Game.Overlays.Dashboard
public enum DashboardOverlayTabs
{
Friends
Friends,
[Description("Currently Playing")]
CurrentlyPlaying
}
}

View File

@ -130,6 +130,11 @@ namespace osu.Game.Overlays
loadDisplay(new FriendDisplay());
break;
case DashboardOverlayTabs.CurrentlyPlaying:
//todo: enable once caching logic is better
//loadDisplay(new CurrentlyPlayingDisplay());
break;
default:
throw new NotImplementedException($"Display for {tab.NewValue} tab is not implemented");
}

View File

@ -27,6 +27,12 @@ namespace osu.Game.Overlays.Settings.Sections.Online
Keywords = new[] { "no-video" },
Current = config.GetBindable<bool>(OsuSetting.PreferNoVideo)
},
new SettingsCheckbox
{
LabelText = "Automatically download beatmaps when spectating",
Keywords = new[] { "spectator" },
Current = config.GetBindable<bool>(OsuSetting.AutomaticallyDownloadWhenSpectating),
},
};
}
}

View File

@ -1,9 +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.
using System;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -104,7 +106,7 @@ namespace osu.Game.Overlays
public OverlayHeaderTabItem(T value)
: base(value)
{
Text.Text = value.ToString().ToLower();
Text.Text = ((Value as Enum)?.GetDescription() ?? Value.ToString()).ToLower();
Text.Font = OsuFont.GetFont(size: 14);
Text.Margin = new MarginPadding { Vertical = 16.5f }; // 15px padding + 1.5px line-height difference compensation
Bar.Margin = new MarginPadding { Bottom = bar_height };

View File

@ -9,20 +9,26 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Spectator;
using osu.Game.Overlays.BeatmapListing.Panels;
using osu.Game.Overlays.Settings;
using osu.Game.Replays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Scoring;
using osu.Game.Screens.Multi.Match.Components;
using osu.Game.Users;
using osuTK;
@ -63,6 +69,12 @@ namespace osu.Game.Screens.Play
private IBindable<WeakReference<BeatmapSetInfo>> managerUpdated;
private TriangleButton watchButton;
private SettingsCheckbox automaticDownload;
private BeatmapSetInfo onlineBeatmap;
/// <summary>
/// Becomes true if a new state is waiting to be loaded (while this screen was not active).
/// </summary>
@ -74,47 +86,91 @@ namespace osu.Game.Screens.Play
}
[BackgroundDependencyLoader]
private void load()
private void load(OsuColour colours, OsuConfigManager config)
{
InternalChildren = new Drawable[]
InternalChild = new Container
{
new FillFlowContainer
Masking = true,
CornerRadius = 20,
AutoSizeAxes = Axes.Both,
AutoSizeDuration = 500,
AutoSizeEasing = Easing.OutQuint,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Spacing = new Vector2(15),
Children = new Drawable[]
new Box
{
new OsuSpriteText
Colour = colours.GreySeafoamDark,
RelativeSizeAxes = Axes.Both,
},
new FillFlowContainer
{
Margin = new MarginPadding(20),
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Spacing = new Vector2(15),
Children = new Drawable[]
{
Text = "Currently spectating",
Font = OsuFont.Default.With(size: 30),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new UserGridPanel(targetUser)
{
Width = 290,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new OsuSpriteText
{
Text = "playing",
Font = OsuFont.Default.With(size: 30),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
beatmapPanelContainer = new Container
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new OsuSpriteText
{
Text = "Spectator Mode",
Font = OsuFont.Default.With(size: 30),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Spacing = new Vector2(15),
Children = new Drawable[]
{
new UserGridPanel(targetUser)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Height = 145,
Width = 290,
},
new SpriteIcon
{
Size = new Vector2(40),
Icon = FontAwesome.Solid.ArrowRight,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
beatmapPanelContainer = new Container
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
}
},
automaticDownload = new SettingsCheckbox
{
LabelText = "Automatically download beatmaps",
Current = config.GetBindable<bool>(OsuSetting.AutomaticallyDownloadWhenSpectating),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
watchButton = new PurpleTriangleButton
{
Text = "Start Watching",
Width = 250,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Action = attemptStart,
Enabled = { Value = false }
}
}
}
},
}
};
}
@ -130,12 +186,14 @@ namespace osu.Game.Screens.Play
managerUpdated = beatmaps.ItemUpdated.GetBoundCopy();
managerUpdated.BindValueChanged(beatmapUpdated);
automaticDownload.Current.BindValueChanged(_ => checkForAutomaticDownload());
}
private void beatmapUpdated(ValueChangedEvent<WeakReference<BeatmapSetInfo>> beatmap)
{
if (beatmap.NewValue.TryGetTarget(out var beatmapSet) && beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID == state.BeatmapID))
attemptStart();
Schedule(attemptStart);
}
private void userSentFrames(int userId, FrameDataBundle data)
@ -189,14 +247,26 @@ namespace osu.Game.Screens.Play
if (userId != targetUser.Id)
return;
if (replay == null) return;
if (replay != null)
{
replay.HasReceivedAllFrames = true;
replay = null;
}
replay.HasReceivedAllFrames = true;
replay = null;
Schedule(clearDisplay);
}
private void clearDisplay()
{
watchButton.Enabled.Value = false;
beatmapPanelContainer.Clear();
}
private void attemptStart()
{
clearDisplay();
showBeatmapPanel(state);
var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.ID == state.RulesetID)?.CreateInstance();
// ruleset not available
@ -210,7 +280,6 @@ namespace osu.Game.Screens.Play
if (resolvedBeatmap == null)
{
showBeatmapPanel(state.BeatmapID.Value);
return;
}
@ -228,6 +297,7 @@ namespace osu.Game.Screens.Play
rulesetInstance = resolvedRuleset;
beatmap.Value = beatmaps.GetWorkingBeatmap(resolvedBeatmap);
watchButton.Enabled.Value = true;
this.Push(new SpectatorPlayerLoader(new Score
{
@ -236,17 +306,43 @@ namespace osu.Game.Screens.Play
}));
}
private void showBeatmapPanel(int beatmapId)
private void showBeatmapPanel(SpectatorState state)
{
var req = new GetBeatmapSetRequest(beatmapId, BeatmapSetLookupType.BeatmapId);
if (state?.BeatmapID == null)
{
beatmapPanelContainer.Clear();
onlineBeatmap = null;
return;
}
var req = new GetBeatmapSetRequest(state.BeatmapID.Value, BeatmapSetLookupType.BeatmapId);
req.Success += res => Schedule(() =>
{
beatmapPanelContainer.Child = new GridBeatmapPanel(res.ToBeatmapSet(rulesets));
if (state != this.state)
return;
onlineBeatmap = res.ToBeatmapSet(rulesets);
beatmapPanelContainer.Child = new GridBeatmapPanel(onlineBeatmap);
checkForAutomaticDownload();
});
api.Queue(req);
}
private void checkForAutomaticDownload()
{
if (onlineBeatmap == null)
return;
if (!automaticDownload.Current.Value)
return;
if (beatmaps.IsAvailableLocally(onlineBeatmap))
return;
beatmaps.Download(onlineBeatmap);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);

View File

@ -3,33 +3,60 @@
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Spectator;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
namespace osu.Game.Screens.Play
{
public class SpectatorPlayer : ReplayPlayer
public class SpectatorPlayer : Player
{
[Resolved]
private SpectatorStreamingClient spectatorStreaming { get; set; }
private readonly Score score;
protected override bool CheckModsAllowFailure() => false; // todo: better support starting mid-way through beatmap
public SpectatorPlayer(Score score)
: base(score)
{
this.score = score;
}
protected override ResultsScreen CreateResults(ScoreInfo score)
{
return new SpectatorResultsScreen(score);
}
[Resolved]
private SpectatorStreamingClient spectatorStreaming { get; set; }
[BackgroundDependencyLoader]
private void load()
{
spectatorStreaming.OnUserBeganPlaying += userBeganPlaying;
AddInternal(new OsuSpriteText
{
Text = $"Watching {score.ScoreInfo.User.Username} playing live!",
Font = OsuFont.Default.With(size: 30),
Y = 100,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
});
}
protected override void PrepareReplay()
{
DrawableRuleset?.SetReplayScore(score);
}
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart)
{
// if we already have frames, start gameplay at the point in time they exist, should they be too far into the beatmap.
double? firstFrameTime = Score.Replay.Frames.FirstOrDefault()?.Time;
double? firstFrameTime = score.Replay.Frames.FirstOrDefault()?.Time;
if (firstFrameTime == null || firstFrameTime <= gameplayStart + 5000)
return base.CreateGameplayClockContainer(beatmap, gameplayStart);
@ -43,6 +70,16 @@ namespace osu.Game.Screens.Play
return base.OnExiting(next);
}
private void userBeganPlaying(int userId, SpectatorState state)
{
if (userId != score.ScoreInfo.UserID) return;
Schedule(() =>
{
if (this.IsCurrentScreen()) this.Exit();
});
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
@ -50,11 +87,5 @@ namespace osu.Game.Screens.Play
if (spectatorStreaming != null)
spectatorStreaming.OnUserBeganPlaying -= userBeganPlaying;
}
private void userBeganPlaying(int userId, SpectatorState state)
{
if (userId == Score.ScoreInfo.UserID)
Schedule(this.Exit);
}
}
}

View File

@ -0,0 +1,47 @@
// 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.Allocation;
using osu.Framework.Screens;
using osu.Game.Online.Spectator;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
namespace osu.Game.Screens.Play
{
public class SpectatorResultsScreen : SoloResultsScreen
{
public SpectatorResultsScreen(ScoreInfo score)
: base(score)
{
}
[Resolved]
private SpectatorStreamingClient spectatorStreaming { get; set; }
[BackgroundDependencyLoader]
private void load()
{
spectatorStreaming.OnUserBeganPlaying += userBeganPlaying;
}
private void userBeganPlaying(int userId, SpectatorState state)
{
if (userId == Score.UserID)
{
Schedule(() =>
{
if (this.IsCurrentScreen()) this.Exit();
});
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (spectatorStreaming != null)
spectatorStreaming.OnUserBeganPlaying -= userBeganPlaying;
}
}
}

View File

@ -20,6 +20,10 @@ namespace osu.Game.Users
{
public readonly User User;
/// <summary>
/// Perform an action in addition to showing the user's profile.
/// This should be used to perform auxiliary tasks and not as a primary action for clicking a user panel (to maintain a consistent UX).
/// </summary>
public new Action Action;
protected Action ViewProfile { get; private set; }