mirror of
https://github.com/ppy/osu.git
synced 2025-01-31 14:25:10 +08:00
Merge pull request #31527 from bdach/spectator-list-ready
Show spectating users during gameplay
This commit is contained in:
commit
8f8246278a
@ -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(),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
BIN
osu.Game.Tests/Resources/Archives/modified-argon-20250116.osk
Normal file
BIN
osu.Game.Tests/Resources/Archives/modified-argon-20250116.osk
Normal file
Binary file not shown.
@ -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>
|
||||||
|
@ -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", () =>
|
||||||
|
{
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
client,
|
||||||
|
new DependencyProvidingContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
CachedDependencies =
|
||||||
|
[
|
||||||
|
(typeof(GameplayState), gameplayState),
|
||||||
|
(typeof(SpectatorClient), client)
|
||||||
|
],
|
||||||
|
Child = list = new SpectatorList
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Spectators = { BindTarget = spectators },
|
}
|
||||||
UserPlayingState = { BindTarget = localUserPlayingState }
|
}
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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());
|
||||||
|
39
osu.Game/Online/Spectator/SpectatorUser.cs
Normal file
39
osu.Game/Online/Spectator/SpectatorUser.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
|
@ -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,16 +109,38 @@ 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[]
|
||||||
|
{
|
||||||
|
new ArgonComboCounter
|
||||||
{
|
{
|
||||||
Anchor = Anchor.BottomLeft,
|
Anchor = Anchor.BottomLeft,
|
||||||
Origin = Anchor.BottomLeft,
|
Origin = Anchor.BottomLeft,
|
||||||
Position = new Vector2(36, -66),
|
|
||||||
Scale = new Vector2(1.3f),
|
Scale = new Vector2(1.3f),
|
||||||
},
|
},
|
||||||
|
new SpectatorList
|
||||||
|
{
|
||||||
|
Anchor = Anchor.BottomLeft,
|
||||||
|
Origin = Anchor.BottomLeft,
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (songProgress != null && keyCounter != null)
|
|
||||||
{
|
|
||||||
const float padding = 10;
|
const float padding = 10;
|
||||||
|
|
||||||
// Hard to find this at runtime, so taken from the most expanded state during replay.
|
// Hard to find this at runtime, so taken from the most expanded state during replay.
|
||||||
const float song_progress_offset_height = 73;
|
const float song_progress_offset_height = 73;
|
||||||
|
|
||||||
|
if (songProgress != null && keyCounter != null)
|
||||||
|
{
|
||||||
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(),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user