1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-31 12:13:00 +08:00

Merge pull request #31527 from bdach/spectator-list-ready

Show spectating users during gameplay
This commit is contained in:
Dan Balasescu 2025-01-21 18:54:11 +09:00 committed by GitHub
commit 8f8246278a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 267 additions and 42 deletions

View File

@ -4,6 +4,7 @@
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -47,6 +48,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
return new DefaultSkinComponentsContainer(container => return new DefaultSkinComponentsContainer(container =>
{ {
var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault(); var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
if (keyCounter != null) if (keyCounter != null)
{ {
@ -55,11 +57,19 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
keyCounter.Origin = Anchor.TopRight; keyCounter.Origin = Anchor.TopRight;
keyCounter.Position = new Vector2(0, -40) * 1.6f; keyCounter.Position = new Vector2(0, -40) * 1.6f;
} }
if (spectatorList != null)
{
spectatorList.Anchor = Anchor.BottomLeft;
spectatorList.Origin = Anchor.BottomLeft;
spectatorList.Position = new Vector2(10, -10);
}
}) })
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
new LegacyKeyCounterDisplay(), new LegacyKeyCounterDisplay(),
new SpectatorList(),
} }
}; };
} }

View File

@ -9,7 +9,9 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Skinning.Argon namespace osu.Game.Rulesets.Mania.Skinning.Argon
@ -39,6 +41,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
return new DefaultSkinComponentsContainer(container => return new DefaultSkinComponentsContainer(container =>
{ {
var combo = container.ChildrenOfType<ArgonManiaComboCounter>().FirstOrDefault(); var combo = container.ChildrenOfType<ArgonManiaComboCounter>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
if (combo != null) if (combo != null)
{ {
@ -47,9 +50,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
combo.Origin = Anchor.Centre; combo.Origin = Anchor.Centre;
combo.Y = 200; combo.Y = 200;
} }
if (spectatorList != null)
spectatorList.Position = new Vector2(36, -66);
}) })
{ {
new ArgonManiaComboCounter(), new ArgonManiaComboCounter(),
new SpectatorList
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
}
}; };
} }

View File

@ -15,7 +15,9 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Mania.Skinning.Legacy namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{ {
@ -95,6 +97,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
return new DefaultSkinComponentsContainer(container => return new DefaultSkinComponentsContainer(container =>
{ {
var combo = container.ChildrenOfType<LegacyManiaComboCounter>().FirstOrDefault(); var combo = container.ChildrenOfType<LegacyManiaComboCounter>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
if (combo != null) if (combo != null)
{ {
@ -102,9 +105,17 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
combo.Origin = Anchor.Centre; combo.Origin = Anchor.Centre;
combo.Y = this.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ComboPosition)?.Value ?? 0; combo.Y = this.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.ComboPosition)?.Value ?? 0;
} }
if (spectatorList != null)
{
spectatorList.Anchor = Anchor.BottomLeft;
spectatorList.Origin = Anchor.BottomLeft;
spectatorList.Position = new Vector2(10, -10);
}
}) })
{ {
new LegacyManiaComboCounter(), new LegacyManiaComboCounter(),
new SpectatorList(),
}; };
} }

View File

@ -6,6 +6,7 @@ using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK; using osuTK;
@ -70,12 +71,24 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
} }
var combo = container.OfType<LegacyDefaultComboCounter>().FirstOrDefault(); var combo = container.OfType<LegacyDefaultComboCounter>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
Vector2 pos = new Vector2();
if (combo != null) if (combo != null)
{ {
combo.Anchor = Anchor.BottomLeft; combo.Anchor = Anchor.BottomLeft;
combo.Origin = Anchor.BottomLeft; combo.Origin = Anchor.BottomLeft;
combo.Scale = new Vector2(1.28f); combo.Scale = new Vector2(1.28f);
pos += new Vector2(10, -(combo.DrawHeight * 1.56f + 20) * combo.Scale.X);
}
if (spectatorList != null)
{
spectatorList.Anchor = Anchor.BottomLeft;
spectatorList.Origin = Anchor.BottomLeft;
spectatorList.Position = pos;
} }
}) })
{ {
@ -83,6 +96,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
new LegacyDefaultComboCounter(), new LegacyDefaultComboCounter(),
new LegacyKeyCounterDisplay(), new LegacyKeyCounterDisplay(),
new SpectatorList(),
} }
}; };
} }

View File

@ -71,6 +71,8 @@ namespace osu.Game.Tests.Skins
"Archives/modified-classic-20240724.osk", "Archives/modified-classic-20240724.osk",
// Covers skinnable mod display // Covers skinnable mod display
"Archives/modified-default-20241207.osk", "Archives/modified-default-20241207.osk",
// Covers skinnable spectator list
"Archives/modified-argon-20250116.osk",
}; };
/// <summary> /// <summary>

View File

@ -6,48 +6,74 @@ using NUnit.Framework;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Visual.Spectator;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
[TestFixture] [TestFixture]
public partial class TestSceneSpectatorList : OsuTestScene public partial class TestSceneSpectatorList : OsuTestScene
{ {
private readonly BindableList<SpectatorList.Spectator> spectators = new BindableList<SpectatorList.Spectator>();
private readonly Bindable<LocalUserPlayingState> localUserPlayingState = new Bindable<LocalUserPlayingState>();
private int counter; private int counter;
[Test] [Test]
public void TestBasics() public void TestBasics()
{ {
SpectatorList list = null!; SpectatorList list = null!;
AddStep("create spectator list", () => Child = list = new SpectatorList Bindable<LocalUserPlayingState> playingState = new Bindable<LocalUserPlayingState>();
GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), healthProcessor: new OsuHealthProcessor(0), localUserPlayingState: playingState);
TestSpectatorClient client = new TestSpectatorClient();
AddStep("create spectator list", () =>
{ {
Anchor = Anchor.Centre, Children = new Drawable[]
Origin = Anchor.Centre, {
Spectators = { BindTarget = spectators }, client,
UserPlayingState = { BindTarget = localUserPlayingState } new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies =
[
(typeof(GameplayState), gameplayState),
(typeof(SpectatorClient), client)
],
Child = list = new SpectatorList
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
}
};
}); });
AddStep("start playing", () => localUserPlayingState.Value = LocalUserPlayingState.Playing); AddStep("start playing", () => playingState.Value = LocalUserPlayingState.Playing);
AddRepeatStep("add a user", () => AddRepeatStep("add a user", () =>
{ {
int id = Interlocked.Increment(ref counter); int id = Interlocked.Increment(ref counter);
spectators.Add(new SpectatorList.Spectator(id, $"User {id}")); ((ISpectatorClient)client).UserStartedWatching([
new SpectatorUser
{
OnlineID = id,
Username = $"User {id}"
}
]);
}, 10); }, 10);
AddRepeatStep("remove random user", () => spectators.RemoveAt(RNG.Next(0, spectators.Count)), 5); AddRepeatStep("remove random user", () => ((ISpectatorClient)client).UserEndedWatching(client.WatchingUsers[RNG.Next(client.WatchingUsers.Count)].OnlineID), 5);
AddStep("change font to venera", () => list.Font.Value = Typeface.Venera); AddStep("change font to venera", () => list.Font.Value = Typeface.Venera);
AddStep("change font to torus", () => list.Font.Value = Typeface.Torus); AddStep("change font to torus", () => list.Font.Value = Typeface.Torus);
AddStep("change header colour", () => list.HeaderColour.Value = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1)); AddStep("change header colour", () => list.HeaderColour.Value = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1));
AddStep("enter break", () => localUserPlayingState.Value = LocalUserPlayingState.Break); AddStep("enter break", () => playingState.Value = LocalUserPlayingState.Break);
AddStep("stop playing", () => localUserPlayingState.Value = LocalUserPlayingState.NotPlaying); AddStep("stop playing", () => playingState.Value = LocalUserPlayingState.NotPlaying);
} }
} }
} }

View File

@ -37,5 +37,17 @@ namespace osu.Game.Online.Spectator
/// <param name="userId">The ID of the user who achieved the score.</param> /// <param name="userId">The ID of the user who achieved the score.</param>
/// <param name="scoreId">The ID of the score.</param> /// <param name="scoreId">The ID of the score.</param>
Task UserScoreProcessed(int userId, long scoreId); Task UserScoreProcessed(int userId, long scoreId);
/// <summary>
/// Signals that another user has <see cref="ISpectatorServer.StartWatchingUser">started watching this client</see>.
/// </summary>
/// <param name="user">The information about the user who started watching.</param>
Task UserStartedWatching(SpectatorUser[] user);
/// <summary>
/// Signals that another user has <see cref="ISpectatorServer.EndWatchingUser">ended watching this client</see>
/// </summary>
/// <param name="userId">The ID of the user who ended watching.</param>
Task UserEndedWatching(int userId);
} }
} }

View File

@ -42,6 +42,8 @@ namespace osu.Game.Online.Spectator
connection.On<int, FrameDataBundle>(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames); connection.On<int, FrameDataBundle>(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames);
connection.On<int, SpectatorState>(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying); connection.On<int, SpectatorState>(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying);
connection.On<int, long>(nameof(ISpectatorClient.UserScoreProcessed), ((ISpectatorClient)this).UserScoreProcessed); connection.On<int, long>(nameof(ISpectatorClient.UserScoreProcessed), ((ISpectatorClient)this).UserScoreProcessed);
connection.On<SpectatorUser[]>(nameof(ISpectatorClient.UserStartedWatching), ((ISpectatorClient)this).UserStartedWatching);
connection.On<int>(nameof(ISpectatorClient.UserEndedWatching), ((ISpectatorClient)this).UserEndedWatching);
connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IStatefulUserHubClient)this).DisconnectRequested); connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IStatefulUserHubClient)this).DisconnectRequested);
}; };

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Development; using osu.Framework.Development;
@ -36,10 +37,16 @@ namespace osu.Game.Online.Spectator
public abstract IBindable<bool> IsConnected { get; } public abstract IBindable<bool> IsConnected { get; }
/// <summary> /// <summary>
/// The states of all users currently being watched. /// The states of all users currently being watched by the local user.
/// </summary> /// </summary>
[UsedImplicitly] // Marked virtual due to mock use in testing
public virtual IBindableDictionary<int, SpectatorState> WatchedUserStates => watchedUserStates; public virtual IBindableDictionary<int, SpectatorState> WatchedUserStates => watchedUserStates;
/// <summary>
/// All users who are currently watching the local user.
/// </summary>
public IBindableList<SpectatorUser> WatchingUsers => watchingUsers;
/// <summary> /// <summary>
/// A global list of all players currently playing. /// A global list of all players currently playing.
/// </summary> /// </summary>
@ -53,6 +60,7 @@ namespace osu.Game.Online.Spectator
/// <summary> /// <summary>
/// Called whenever new frames arrive from the server. /// Called whenever new frames arrive from the server.
/// </summary> /// </summary>
[UsedImplicitly] // Marked virtual due to mock use in testing
public virtual event Action<int, FrameDataBundle>? OnNewFrames; public virtual event Action<int, FrameDataBundle>? OnNewFrames;
/// <summary> /// <summary>
@ -82,6 +90,7 @@ namespace osu.Game.Online.Spectator
private readonly BindableDictionary<int, SpectatorState> watchedUserStates = new BindableDictionary<int, SpectatorState>(); private readonly BindableDictionary<int, SpectatorState> watchedUserStates = new BindableDictionary<int, SpectatorState>();
private readonly BindableList<SpectatorUser> watchingUsers = new BindableList<SpectatorUser>();
private readonly BindableList<int> playingUsers = new BindableList<int>(); private readonly BindableList<int> playingUsers = new BindableList<int>();
private readonly SpectatorState currentState = new SpectatorState(); private readonly SpectatorState currentState = new SpectatorState();
@ -127,6 +136,7 @@ namespace osu.Game.Online.Spectator
{ {
playingUsers.Clear(); playingUsers.Clear();
watchedUserStates.Clear(); watchedUserStates.Clear();
watchingUsers.Clear();
} }
}), true); }), true);
} }
@ -179,6 +189,30 @@ namespace osu.Game.Online.Spectator
return Task.CompletedTask; return Task.CompletedTask;
} }
Task ISpectatorClient.UserStartedWatching(SpectatorUser[] users)
{
Schedule(() =>
{
foreach (var user in users)
{
if (!watchingUsers.Contains(user))
watchingUsers.Add(user);
}
});
return Task.CompletedTask;
}
Task ISpectatorClient.UserEndedWatching(int userId)
{
Schedule(() =>
{
watchingUsers.RemoveAll(u => u.OnlineID == userId);
});
return Task.CompletedTask;
}
Task IStatefulUserHubClient.DisconnectRequested() Task IStatefulUserHubClient.DisconnectRequested()
{ {
Schedule(() => DisconnectInternal()); Schedule(() => DisconnectInternal());

View File

@ -0,0 +1,39 @@
// 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 MessagePack;
using osu.Game.Users;
namespace osu.Game.Online.Spectator
{
[Serializable]
[MessagePackObject]
public class SpectatorUser : IUser, IEquatable<SpectatorUser>
{
[Key(0)]
public int OnlineID { get; set; }
[Key(1)]
public string Username { get; set; } = string.Empty;
[IgnoreMember]
public CountryCode CountryCode => CountryCode.Unknown;
[IgnoreMember]
public bool IsBot => false;
public bool Equals(SpectatorUser? other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
return OnlineID == other.OnlineID;
}
public override bool Equals(object? obj) => Equals(obj as SpectatorUser);
// ReSharper disable once NonReadonlyMemberInGetHashCode
public override int GetHashCode() => OnlineID;
}
}

View File

@ -69,6 +69,11 @@ namespace osu.Game.Screens.Play
private readonly Bindable<JudgementResult> lastJudgementResult = new Bindable<JudgementResult>(); private readonly Bindable<JudgementResult> lastJudgementResult = new Bindable<JudgementResult>();
/// <summary>
/// The local user's playing state (whether actively playing, paused, or not playing due to watching a replay or similar).
/// </summary>
public IBindable<LocalUserPlayingState> PlayingState { get; } = new Bindable<LocalUserPlayingState>();
public GameplayState( public GameplayState(
IBeatmap beatmap, IBeatmap beatmap,
Ruleset ruleset, Ruleset ruleset,
@ -76,7 +81,8 @@ namespace osu.Game.Screens.Play
Score? score = null, Score? score = null,
ScoreProcessor? scoreProcessor = null, ScoreProcessor? scoreProcessor = null,
HealthProcessor? healthProcessor = null, HealthProcessor? healthProcessor = null,
Storyboard? storyboard = null) Storyboard? storyboard = null,
IBindable<LocalUserPlayingState>? localUserPlayingState = null)
{ {
Beatmap = beatmap; Beatmap = beatmap;
Ruleset = ruleset; Ruleset = ruleset;
@ -92,6 +98,9 @@ namespace osu.Game.Screens.Play
ScoreProcessor = scoreProcessor ?? ruleset.CreateScoreProcessor(); ScoreProcessor = scoreProcessor ?? ruleset.CreateScoreProcessor();
HealthProcessor = healthProcessor ?? ruleset.CreateHealthProcessor(beatmap.HitObjects[0].StartTime); HealthProcessor = healthProcessor ?? ruleset.CreateHealthProcessor(beatmap.HitObjects[0].StartTime);
Storyboard = storyboard ?? new Storyboard(); Storyboard = storyboard ?? new Storyboard();
if (localUserPlayingState != null)
PlayingState.BindTo(localUserPlayingState);
} }
/// <summary> /// <summary>

View File

@ -15,18 +15,20 @@ using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Online.Chat; using osu.Game.Online.Chat;
using osu.Game.Users;
using osu.Game.Localisation.HUD; using osu.Game.Localisation.HUD;
using osu.Game.Localisation.SkinComponents; using osu.Game.Localisation.SkinComponents;
using osu.Game.Online.Spectator;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
public partial class SpectatorList : CompositeDrawable public partial class SpectatorList : CompositeDrawable, ISerialisableDrawable
{ {
private const int max_spectators_displayed = 10; private const int max_spectators_displayed = 10;
public BindableList<Spectator> Spectators { get; } = new BindableList<Spectator>(); public BindableList<SpectatorUser> Spectators { get; } = new BindableList<SpectatorUser>();
public Bindable<LocalUserPlayingState> UserPlayingState { get; } = new Bindable<LocalUserPlayingState>(); public Bindable<LocalUserPlayingState> UserPlayingState { get; } = new Bindable<LocalUserPlayingState>();
[SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))] [SettingSource(typeof(SkinnableComponentStrings), nameof(SkinnableComponentStrings.Font), nameof(SkinnableComponentStrings.FontDescription))]
@ -41,13 +43,20 @@ namespace osu.Game.Screens.Play.HUD
private FillFlowContainer<SpectatorListEntry> spectatorsFlow = null!; private FillFlowContainer<SpectatorListEntry> spectatorsFlow = null!;
private DrawablePool<SpectatorListEntry> pool = null!; private DrawablePool<SpectatorListEntry> pool = null!;
[Resolved]
private SpectatorClient client { get; set; } = null!;
[Resolved]
private GameplayState gameplayState { get; set; } = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
InternalChildren = new Drawable[] InternalChildren = new[]
{ {
Empty().With(t => t.Size = new Vector2(100, 50)),
mainFlow = new FillFlowContainer mainFlow = new FillFlowContainer
{ {
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
@ -76,6 +85,9 @@ namespace osu.Game.Screens.Play.HUD
{ {
base.LoadComplete(); base.LoadComplete();
((IBindableList<SpectatorUser>)Spectators).BindTo(client.WatchingUsers);
((IBindable<LocalUserPlayingState>)UserPlayingState).BindTo(gameplayState.PlayingState);
Spectators.BindCollectionChanged(onSpectatorsChanged, true); Spectators.BindCollectionChanged(onSpectatorsChanged, true);
UserPlayingState.BindValueChanged(_ => updateVisibility()); UserPlayingState.BindValueChanged(_ => updateVisibility());
@ -94,7 +106,7 @@ namespace osu.Game.Screens.Play.HUD
{ {
for (int i = 0; i < e.NewItems!.Count; i++) for (int i = 0; i < e.NewItems!.Count; i++)
{ {
var spectator = (Spectator)e.NewItems![i]!; var spectator = (SpectatorUser)e.NewItems![i]!;
int index = Math.Max(e.NewStartingIndex, 0) + i; int index = Math.Max(e.NewStartingIndex, 0) + i;
if (index >= max_spectators_displayed) if (index >= max_spectators_displayed)
@ -143,7 +155,7 @@ namespace osu.Game.Screens.Play.HUD
} }
} }
private void addNewSpectatorToList(int i, Spectator spectator) private void addNewSpectatorToList(int i, SpectatorUser spectator)
{ {
var entry = pool.Get(entry => var entry = pool.Get(entry =>
{ {
@ -156,6 +168,7 @@ namespace osu.Game.Screens.Play.HUD
private void updateVisibility() private void updateVisibility()
{ {
// We don't want to show spectators when we are watching a replay.
mainFlow.FadeTo(Spectators.Count > 0 && UserPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint); mainFlow.FadeTo(Spectators.Count > 0 && UserPlayingState.Value != LocalUserPlayingState.NotPlaying ? 1 : 0, 250, Easing.OutQuint);
} }
@ -169,7 +182,7 @@ namespace osu.Game.Screens.Play.HUD
private partial class SpectatorListEntry : PoolableDrawable private partial class SpectatorListEntry : PoolableDrawable
{ {
public Bindable<Spectator> Current { get; } = new Bindable<Spectator>(); public Bindable<SpectatorUser> Current { get; } = new Bindable<SpectatorUser>();
private readonly BindableWithCurrent<LocalUserPlayingState> current = new BindableWithCurrent<LocalUserPlayingState>(); private readonly BindableWithCurrent<LocalUserPlayingState> current = new BindableWithCurrent<LocalUserPlayingState>();
@ -233,10 +246,6 @@ namespace osu.Game.Screens.Play.HUD
} }
} }
public record Spectator(int OnlineID, string Username) : IUser public bool UsesFixedAnchor { get; set; }
{
public CountryCode CountryCode => CountryCode.Unknown;
public bool IsBot => false;
}
} }
} }

View File

@ -261,7 +261,7 @@ namespace osu.Game.Screens.Play
Score.ScoreInfo.Ruleset = ruleset.RulesetInfo; Score.ScoreInfo.Ruleset = ruleset.RulesetInfo;
Score.ScoreInfo.Mods = gameplayMods; Score.ScoreInfo.Mods = gameplayMods;
dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score, ScoreProcessor, HealthProcessor, Beatmap.Value.Storyboard)); dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score, ScoreProcessor, HealthProcessor, Beatmap.Value.Storyboard, PlayingState));
var rulesetSkinProvider = new RulesetSkinProvidingContainer(ruleset, playableBeatmap, Beatmap.Value.Skin); var rulesetSkinProvider = new RulesetSkinProvidingContainer(ruleset, playableBeatmap, Beatmap.Value.Skin);
GameplayClockContainer.Add(new GameplayScrollWheelHandling()); GameplayClockContainer.Add(new GameplayScrollWheelHandling());

View File

@ -7,7 +7,6 @@ using JetBrains.Annotations;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
@ -110,15 +109,37 @@ namespace osu.Game.Skinning
case GlobalSkinnableContainers.MainHUDComponents: case GlobalSkinnableContainers.MainHUDComponents:
if (containerLookup.Ruleset != null) if (containerLookup.Ruleset != null)
{ {
return new Container return new DefaultSkinComponentsContainer(container =>
{
var comboCounter = container.OfType<ArgonComboCounter>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
Vector2 pos = new Vector2(36, -66);
if (comboCounter != null)
{
comboCounter.Position = pos;
pos -= new Vector2(0, comboCounter.DrawHeight * 1.4f + 20);
}
if (spectatorList != null)
spectatorList.Position = pos;
})
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = new ArgonComboCounter Children = new Drawable[]
{ {
Anchor = Anchor.BottomLeft, new ArgonComboCounter
Origin = Anchor.BottomLeft, {
Position = new Vector2(36, -66), Anchor = Anchor.BottomLeft,
Scale = new Vector2(1.3f), Origin = Anchor.BottomLeft,
Scale = new Vector2(1.3f),
},
new SpectatorList
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
}
}, },
}; };
} }

View File

@ -367,16 +367,29 @@ namespace osu.Game.Skinning
return new DefaultSkinComponentsContainer(container => return new DefaultSkinComponentsContainer(container =>
{ {
var combo = container.OfType<LegacyDefaultComboCounter>().FirstOrDefault(); var combo = container.OfType<LegacyDefaultComboCounter>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
Vector2 pos = new Vector2();
if (combo != null) if (combo != null)
{ {
combo.Anchor = Anchor.BottomLeft; combo.Anchor = Anchor.BottomLeft;
combo.Origin = Anchor.BottomLeft; combo.Origin = Anchor.BottomLeft;
combo.Scale = new Vector2(1.28f); combo.Scale = new Vector2(1.28f);
pos += new Vector2(10, -(combo.DrawHeight * 1.56f + 20) * combo.Scale.X);
}
if (spectatorList != null)
{
spectatorList.Anchor = Anchor.BottomLeft;
spectatorList.Origin = Anchor.BottomLeft;
spectatorList.Position = pos;
} }
}) })
{ {
new LegacyDefaultComboCounter() new LegacyDefaultComboCounter(),
new SpectatorList(),
}; };
} }

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Textures;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.HitErrorMeters; using osu.Game.Screens.Play.HUD.HitErrorMeters;
@ -90,6 +91,7 @@ namespace osu.Game.Skinning
var ppCounter = container.OfType<PerformancePointsCounter>().FirstOrDefault(); var ppCounter = container.OfType<PerformancePointsCounter>().FirstOrDefault();
var songProgress = container.OfType<DefaultSongProgress>().FirstOrDefault(); var songProgress = container.OfType<DefaultSongProgress>().FirstOrDefault();
var keyCounter = container.OfType<DefaultKeyCounterDisplay>().FirstOrDefault(); var keyCounter = container.OfType<DefaultKeyCounterDisplay>().FirstOrDefault();
var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
if (score != null) if (score != null)
{ {
@ -142,17 +144,26 @@ namespace osu.Game.Skinning
} }
} }
const float padding = 10;
// Hard to find this at runtime, so taken from the most expanded state during replay.
const float song_progress_offset_height = 73;
if (songProgress != null && keyCounter != null) if (songProgress != null && keyCounter != null)
{ {
const float padding = 10;
// Hard to find this at runtime, so taken from the most expanded state during replay.
const float song_progress_offset_height = 73;
keyCounter.Anchor = Anchor.BottomRight; keyCounter.Anchor = Anchor.BottomRight;
keyCounter.Origin = Anchor.BottomRight; keyCounter.Origin = Anchor.BottomRight;
keyCounter.Position = new Vector2(-padding, -(song_progress_offset_height + padding)); keyCounter.Position = new Vector2(-padding, -(song_progress_offset_height + padding));
} }
if (spectatorList != null)
{
spectatorList.Font.Value = Typeface.Venera;
spectatorList.HeaderColour.Value = new OsuColour().BlueLighter;
spectatorList.Anchor = Anchor.BottomLeft;
spectatorList.Origin = Anchor.BottomLeft;
spectatorList.Position = new Vector2(padding, -(song_progress_offset_height + padding));
}
}) })
{ {
Children = new Drawable[] Children = new Drawable[]
@ -165,7 +176,8 @@ namespace osu.Game.Skinning
new DefaultKeyCounterDisplay(), new DefaultKeyCounterDisplay(),
new BarHitErrorMeter(), new BarHitErrorMeter(),
new BarHitErrorMeter(), new BarHitErrorMeter(),
new TrianglesPerformancePointsCounter() new TrianglesPerformancePointsCounter(),
new SpectatorList(),
} }
}; };