1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-15 02:42:54 +08:00

Merge branch 'master' into fix-osu-editor-exception

This commit is contained in:
Dean Herbert 2021-08-12 12:15:04 +09:00 committed by GitHub
commit ee4fc049bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 737 additions and 110 deletions

View File

@ -52,7 +52,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.810.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.810.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.810.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.811.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -6,9 +6,11 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
{ {
@ -31,7 +33,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
}; };
foreach (var (userId, _) in clocks) foreach (var (userId, _) in clocks)
{
SpectatorClient.StartPlay(userId, 0); SpectatorClient.StartPlay(userId, 0);
OnlinePlayDependencies.Client.AddUser(new User { Id = userId });
}
}); });
AddStep("create leaderboard", () => AddStep("create leaderboard", () =>
@ -41,7 +46,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
var scoreProcessor = new OsuScoreProcessor(); var scoreProcessor = new OsuScoreProcessor();
scoreProcessor.ApplyBeatmap(playable); scoreProcessor.ApplyBeatmap(playable);
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, clocks.Keys.ToArray()) { Expanded = { Value = true } }, Add); LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray()) { Expanded = { Value = true } }, Add);
}); });
AddUntilStep("wait for load", () => leaderboard.IsLoaded); AddUntilStep("wait for load", () => leaderboard.IsLoaded);

View File

@ -8,6 +8,8 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate; using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -26,7 +28,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private MultiSpectatorScreen spectatorScreen; private MultiSpectatorScreen spectatorScreen;
private readonly List<int> playingUserIds = new List<int>(); private readonly List<MultiplayerRoomUser> playingUsers = new List<MultiplayerRoomUser>();
private BeatmapSetInfo importedSet; private BeatmapSetInfo importedSet;
private BeatmapInfo importedBeatmap; private BeatmapInfo importedBeatmap;
@ -41,7 +43,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
[SetUp] [SetUp]
public new void Setup() => Schedule(() => playingUserIds.Clear()); public new void Setup() => Schedule(() => playingUsers.Clear());
[Test] [Test]
public void TestDelayedStart() public void TestDelayedStart()
@ -51,8 +53,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_1_ID }, true); OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_1_ID }, true);
OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_2_ID }, true); OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_2_ID }, true);
playingUserIds.Add(PLAYER_1_ID); playingUsers.Add(new MultiplayerRoomUser(PLAYER_1_ID));
playingUserIds.Add(PLAYER_2_ID); playingUsers.Add(new MultiplayerRoomUser(PLAYER_2_ID));
}); });
loadSpectateScreen(false); loadSpectateScreen(false);
@ -78,6 +80,38 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddWaitStep("wait a bit", 20); AddWaitStep("wait a bit", 20);
} }
[Test]
public void TestTeamDisplay()
{
AddStep("start players", () =>
{
var player1 = OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_1_ID }, true);
player1.MatchState = new TeamVersusUserState
{
TeamID = 0,
};
var player2 = OnlinePlayDependencies.Client.AddUser(new User { Id = PLAYER_2_ID }, true);
player2.MatchState = new TeamVersusUserState
{
TeamID = 1,
};
SpectatorClient.StartPlay(player1.UserID, importedBeatmapId);
SpectatorClient.StartPlay(player2.UserID, importedBeatmapId);
playingUsers.Add(player1);
playingUsers.Add(player2);
});
loadSpectateScreen();
sendFrames(PLAYER_1_ID, 1000);
sendFrames(PLAYER_2_ID, 1000);
AddWaitStep("wait a bit", 20);
}
[Test] [Test]
public void TestTimeDoesNotProgressWhileAllPlayersPaused() public void TestTimeDoesNotProgressWhileAllPlayersPaused()
{ {
@ -254,7 +288,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Beatmap.Value = beatmapManager.GetWorkingBeatmap(importedBeatmap); Beatmap.Value = beatmapManager.GetWorkingBeatmap(importedBeatmap);
Ruleset.Value = importedBeatmap.Ruleset; Ruleset.Value = importedBeatmap.Ruleset;
LoadScreen(spectatorScreen = new MultiSpectatorScreen(playingUserIds.ToArray())); LoadScreen(spectatorScreen = new MultiSpectatorScreen(playingUsers.ToArray()));
}); });
AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectatorScreen.AllPlayersLoaded)); AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectatorScreen.AllPlayersLoaded));
@ -269,7 +303,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
OnlinePlayDependencies.Client.AddUser(new User { Id = id }, true); OnlinePlayDependencies.Client.AddUser(new User { Id = id }, true);
SpectatorClient.StartPlay(id, beatmapId ?? importedBeatmapId); SpectatorClient.StartPlay(id, beatmapId ?? importedBeatmapId);
playingUserIds.Add(id); playingUsers.Add(new MultiplayerRoomUser(id));
} }
}); });
} }

View File

@ -12,6 +12,7 @@ using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Spectator; using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy; using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Osu.Scoring;
@ -51,12 +52,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
OsuScoreProcessor scoreProcessor; OsuScoreProcessor scoreProcessor;
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
var multiplayerUsers = new List<MultiplayerRoomUser>();
foreach (var user in users) foreach (var user in users)
{ {
SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0); SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0);
OnlinePlayDependencies.Client.AddUser(new User { Id = user }, true); multiplayerUsers.Add(OnlinePlayDependencies.Client.AddUser(new User { Id = user }, true));
} }
Children = new Drawable[] Children = new Drawable[]
@ -64,9 +66,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
scoreProcessor = new OsuScoreProcessor(), scoreProcessor = new OsuScoreProcessor(),
}; };
scoreProcessor.ApplyBeatmap(playable); scoreProcessor.ApplyBeatmap(playableBeatmap);
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, users.ToArray()) LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, multiplayerUsers.ToArray())
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -0,0 +1,121 @@
// 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.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Play.HUD;
using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Tests.Visual.Spectator;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerGameplayLeaderboardTeams : MultiplayerTestScene
{
private static IEnumerable<int> users => Enumerable.Range(0, 16);
public new TestSceneMultiplayerGameplayLeaderboard.TestMultiplayerSpectatorClient SpectatorClient =>
(TestSceneMultiplayerGameplayLeaderboard.TestMultiplayerSpectatorClient)OnlinePlayDependencies?.SpectatorClient;
protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies();
protected class TestDependencies : MultiplayerTestSceneDependencies
{
protected override TestSpectatorClient CreateSpectatorClient() => new TestSceneMultiplayerGameplayLeaderboard.TestMultiplayerSpectatorClient();
}
private MultiplayerGameplayLeaderboard leaderboard;
private GameplayMatchScoreDisplay gameplayScoreDisplay;
protected override Room CreateRoom()
{
var room = base.CreateRoom();
room.Type.Value = MatchType.TeamVersus;
return room;
}
[SetUpSteps]
public override void SetUpSteps()
{
AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = LookupCache.GetUserAsync(1).Result);
AddStep("create leaderboard", () =>
{
leaderboard?.Expire();
OsuScoreProcessor scoreProcessor;
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
var multiplayerUsers = new List<MultiplayerRoomUser>();
foreach (var user in users)
{
SpectatorClient.StartPlay(user, Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0);
var roomUser = OnlinePlayDependencies.Client.AddUser(new User { Id = user }, true);
roomUser.MatchState = new TeamVersusUserState
{
TeamID = RNG.Next(0, 2)
};
multiplayerUsers.Add(roomUser);
}
Children = new Drawable[]
{
scoreProcessor = new OsuScoreProcessor(),
};
scoreProcessor.ApplyBeatmap(playableBeatmap);
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, multiplayerUsers.ToArray())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}, gameplayLeaderboard =>
{
LoadComponentAsync(new MatchScoreDisplay
{
Team1Score = { BindTarget = leaderboard.TeamScores[0] },
Team2Score = { BindTarget = leaderboard.TeamScores[1] }
}, Add);
LoadComponentAsync(gameplayScoreDisplay = new GameplayMatchScoreDisplay
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Team1Score = { BindTarget = leaderboard.TeamScores[0] },
Team2Score = { BindTarget = leaderboard.TeamScores[1] }
}, Add);
Add(gameplayLeaderboard);
});
});
AddUntilStep("wait for load", () => leaderboard.IsLoaded);
AddUntilStep("wait for user population", () => Client.CurrentMatchPlayingUserIds.Count > 0);
}
[Test]
public void TestScoreUpdates()
{
AddRepeatStep("update state", () => SpectatorClient.RandomlyUpdateState(), 100);
AddToggleStep("switch compact mode", expanded =>
{
leaderboard.Expanded.Value = expanded;
gameplayScoreDisplay.Expanded.Value = expanded;
});
}
}
}

View File

@ -155,6 +155,42 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("second user crown visible", () => this.ChildrenOfType<ParticipantPanel>().ElementAt(1).ChildrenOfType<SpriteIcon>().First().Alpha == 1); AddUntilStep("second user crown visible", () => this.ChildrenOfType<ParticipantPanel>().ElementAt(1).ChildrenOfType<SpriteIcon>().First().Alpha == 1);
} }
[Test]
public void TestKickButtonOnlyPresentWhenHost()
{
AddStep("add user", () => Client.AddUser(new User
{
Id = 3,
Username = "Second",
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}));
AddUntilStep("kick buttons visible", () => this.ChildrenOfType<ParticipantPanel.KickButton>().Count(d => d.IsPresent) == 1);
AddStep("make second user host", () => Client.TransferHost(3));
AddUntilStep("kick buttons not visible", () => this.ChildrenOfType<ParticipantPanel.KickButton>().Count(d => d.IsPresent) == 0);
AddStep("make local user host again", () => Client.TransferHost(API.LocalUser.Value.Id));
AddUntilStep("kick buttons visible", () => this.ChildrenOfType<ParticipantPanel.KickButton>().Count(d => d.IsPresent) == 1);
}
[Test]
public void TestKickButtonKicks()
{
AddStep("add user", () => Client.AddUser(new User
{
Id = 3,
Username = "Second",
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}));
AddStep("kick second user", () => this.ChildrenOfType<ParticipantPanel.KickButton>().Single(d => d.IsPresent).TriggerClick());
AddAssert("second user kicked", () => Client.Room?.Users.Single().UserID == API.LocalUser.Value.Id);
}
[Test] [Test]
public void TestManyUsers() public void TestManyUsers()
{ {

View File

@ -16,7 +16,7 @@ namespace osu.Game.Tournament.Tests.Components
public TestSceneMatchScoreDisplay() public TestSceneMatchScoreDisplay()
{ {
Add(new MatchScoreDisplay Add(new TournamentMatchScoreDisplay
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -16,7 +16,8 @@ using osuTK;
namespace osu.Game.Tournament.Screens.Gameplay.Components namespace osu.Game.Tournament.Screens.Gameplay.Components
{ {
public class MatchScoreDisplay : CompositeDrawable // TODO: Update to derive from osu-side class?
public class TournamentMatchScoreDisplay : CompositeDrawable
{ {
private const float bar_height = 18; private const float bar_height = 18;
@ -29,7 +30,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
private readonly Drawable score1Bar; private readonly Drawable score1Bar;
private readonly Drawable score2Bar; private readonly Drawable score2Bar;
public MatchScoreDisplay() public TournamentMatchScoreDisplay()
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;

View File

@ -86,7 +86,7 @@ namespace osu.Game.Tournament.Screens.Gameplay
}, },
} }
}, },
scoreDisplay = new MatchScoreDisplay scoreDisplay = new TournamentMatchScoreDisplay
{ {
Y = -147, Y = -147,
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
@ -148,7 +148,7 @@ namespace osu.Game.Tournament.Screens.Gameplay
} }
private ScheduledDelegate scheduledOperation; private ScheduledDelegate scheduledOperation;
private MatchScoreDisplay scoreDisplay; private TournamentMatchScoreDisplay scoreDisplay;
private TourneyState lastState; private TourneyState lastState;
private MatchHeader header; private MatchHeader header;

View File

@ -26,8 +26,8 @@ namespace osu.Game.Tournament
{ {
public static ColourInfo GetTeamColour(TeamColour teamColour) => teamColour == TeamColour.Red ? COLOUR_RED : COLOUR_BLUE; public static ColourInfo GetTeamColour(TeamColour teamColour) => teamColour == TeamColour.Red ? COLOUR_RED : COLOUR_BLUE;
public static readonly Color4 COLOUR_RED = Color4Extensions.FromHex("#AA1414"); public static readonly Color4 COLOUR_RED = new OsuColour().TeamColourRed;
public static readonly Color4 COLOUR_BLUE = Color4Extensions.FromHex("#1462AA"); public static readonly Color4 COLOUR_BLUE = new OsuColour().TeamColourBlue;
public static readonly Color4 ELEMENT_BACKGROUND_COLOUR = Color4Extensions.FromHex("#fff"); public static readonly Color4 ELEMENT_BACKGROUND_COLOUR = Color4Extensions.FromHex("#fff");
public static readonly Color4 ELEMENT_FOREGROUND_COLOUR = Color4Extensions.FromHex("#000"); public static readonly Color4 ELEMENT_FOREGROUND_COLOUR = Color4Extensions.FromHex("#000");

View File

@ -130,6 +130,9 @@ namespace osu.Game.Graphics
return Gray(brightness > 0.5f ? 0.2f : 0.9f); return Gray(brightness > 0.5f ? 0.2f : 0.9f);
} }
public readonly Color4 TeamColourRed = Color4Extensions.FromHex("#AA1414");
public readonly Color4 TeamColourBlue = Color4Extensions.FromHex("#1462AA");
// See https://github.com/ppy/osu-web/blob/master/resources/assets/less/colors.less // See https://github.com/ppy/osu-web/blob/master/resources/assets/less/colors.less
public readonly Color4 PurpleLighter = Color4Extensions.FromHex(@"eeeeff"); public readonly Color4 PurpleLighter = Color4Extensions.FromHex(@"eeeeff");
public readonly Color4 PurpleLight = Color4Extensions.FromHex(@"aa88ff"); public readonly Color4 PurpleLight = Color4Extensions.FromHex(@"aa88ff");

View File

@ -27,6 +27,14 @@ namespace osu.Game.Online.Multiplayer
/// <exception cref="NotJoinedRoomException">If the user is not in a room.</exception> /// <exception cref="NotJoinedRoomException">If the user is not in a room.</exception>
Task TransferHost(int userId); Task TransferHost(int userId);
/// <summary>
/// As the host, kick another user from the room.
/// </summary>
/// <param name="userId">The user to kick..</param>
/// <exception cref="NotHostException">A user other than the current host is attempting to kick a user.</exception>
/// <exception cref="NotJoinedRoomException">If the user is not in a room.</exception>
Task KickUser(int userId);
/// <summary> /// <summary>
/// As the host, update the settings of the currently joined room. /// As the host, update the settings of the currently joined room.
/// </summary> /// </summary>

View File

@ -293,6 +293,8 @@ namespace osu.Game.Online.Multiplayer
public abstract Task TransferHost(int userId); public abstract Task TransferHost(int userId);
public abstract Task KickUser(int userId);
public abstract Task ChangeSettings(MultiplayerRoomSettings settings); public abstract Task ChangeSettings(MultiplayerRoomSettings settings);
public abstract Task ChangeState(MultiplayerUserState newState); public abstract Task ChangeState(MultiplayerUserState newState);

View File

@ -91,6 +91,14 @@ namespace osu.Game.Online.Multiplayer
return connection.InvokeAsync(nameof(IMultiplayerServer.TransferHost), userId); return connection.InvokeAsync(nameof(IMultiplayerServer.TransferHost), userId);
} }
public override Task KickUser(int userId)
{
if (!IsConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.KickUser), userId);
}
public override Task ChangeSettings(MultiplayerRoomSettings settings) public override Task ChangeSettings(MultiplayerRoomSettings settings)
{ {
if (!IsConnected.Value) if (!IsConnected.Value)

View File

@ -0,0 +1,40 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Game.Screens.Play.HUD;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
public class GameplayMatchScoreDisplay : MatchScoreDisplay
{
public Bindable<bool> Expanded = new Bindable<bool>();
protected override void LoadComplete()
{
base.LoadComplete();
Scale = new Vector2(0.5f);
Expanded.BindValueChanged(expandedChanged, true);
}
private void expandedChanged(ValueChangedEvent<bool> expanded)
{
if (expanded.NewValue)
{
Score1Text.FadeIn(500, Easing.OutQuint);
Score2Text.FadeIn(500, Easing.OutQuint);
this.ResizeWidthTo(2, 500, Easing.OutQuint);
}
else
{
Score1Text.FadeOut(500, Easing.OutQuint);
Score2Text.FadeOut(500, Easing.OutQuint);
this.ResizeWidthTo(1, 500, Easing.OutQuint);
}
}
}
}

View File

@ -475,16 +475,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override Screen CreateGameplayScreen() protected override Screen CreateGameplayScreen()
{ {
Debug.Assert(client.LocalUser != null); Debug.Assert(client.LocalUser != null);
Debug.Assert(client.Room != null);
int[] userIds = client.CurrentMatchPlayingUserIds.ToArray(); int[] userIds = client.CurrentMatchPlayingUserIds.ToArray();
MultiplayerRoomUser[] users = userIds.Select(id => client.Room.Users.First(u => u.UserID == id)).ToArray();
switch (client.LocalUser.State) switch (client.LocalUser.State)
{ {
case MultiplayerUserState.Spectating: case MultiplayerUserState.Spectating:
return new MultiSpectatorScreen(userIds); return new MultiSpectatorScreen(users.Take(PlayerGrid.MAX_PLAYERS).ToArray());
default: default:
return new PlayerLoader(() => new MultiplayerPlayer(SelectedItem.Value, userIds)); return new PlayerLoader(() => new MultiplayerPlayer(SelectedItem.Value, users));
} }
} }

View File

@ -3,9 +3,12 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
@ -34,16 +37,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private MultiplayerGameplayLeaderboard leaderboard; private MultiplayerGameplayLeaderboard leaderboard;
private readonly int[] userIds; private readonly MultiplayerRoomUser[] users;
private LoadingLayer loadingDisplay; private LoadingLayer loadingDisplay;
private FillFlowContainer leaderboardFlow;
/// <summary> /// <summary>
/// Construct a multiplayer player. /// Construct a multiplayer player.
/// </summary> /// </summary>
/// <param name="playlistItem">The playlist item to be played.</param> /// <param name="playlistItem">The playlist item to be played.</param>
/// <param name="userIds">The users which are participating in this game.</param> /// <param name="users">The users which are participating in this game.</param>
public MultiplayerPlayer(PlaylistItem playlistItem, int[] userIds) public MultiplayerPlayer(PlaylistItem playlistItem, MultiplayerRoomUser[] users)
: base(playlistItem, new PlayerConfiguration : base(playlistItem, new PlayerConfiguration
{ {
AllowPause = false, AllowPause = false,
@ -51,14 +55,41 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
AllowSkipping = false, AllowSkipping = false,
}) })
{ {
this.userIds = userIds; this.users = users;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
if (!LoadedBeatmapSuccessfully)
return;
HUDOverlay.Add(leaderboardFlow = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
});
// todo: this should be implemented via a custom HUD implementation, and correctly masked to the main content area. // todo: this should be implemented via a custom HUD implementation, and correctly masked to the main content area.
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(ScoreProcessor, userIds), HUDOverlay.Add); LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(ScoreProcessor, users), l =>
{
if (!LoadedBeatmapSuccessfully)
return;
((IBindable<bool>)leaderboard.Expanded).BindTo(HUDOverlay.ShowHud);
leaderboardFlow.Add(l);
if (leaderboard.TeamScores.Count >= 2)
{
LoadComponentAsync(new GameplayMatchScoreDisplay
{
Team1Score = { BindTarget = leaderboard.TeamScores.First().Value },
Team2Score = { BindTarget = leaderboard.TeamScores.Last().Value },
Expanded = { BindTarget = HUDOverlay.ShowHud },
}, leaderboardFlow.Add);
}
});
HUDOverlay.Add(loadingDisplay = new LoadingLayer(true) { Depth = float.MaxValue }); HUDOverlay.Add(loadingDisplay = new LoadingLayer(true) { Depth = float.MaxValue });
} }
@ -67,6 +98,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{ {
base.LoadAsyncComplete(); base.LoadAsyncComplete();
if (!LoadedBeatmapSuccessfully)
return;
if (!ValidForResume) if (!ValidForResume)
return; // token retrieval may have failed. return; // token retrieval may have failed.
@ -92,13 +126,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Debug.Assert(client.Room != null); Debug.Assert(client.Room != null);
} }
protected override void LoadComplete()
{
base.LoadComplete();
((IBindable<bool>)leaderboard.Expanded).BindTo(HUDOverlay.ShowHud);
}
protected override void StartGameplay() protected override void StartGameplay()
{ {
// block base call, but let the server know we are ready to start. // block base call, but let the server know we are ready to start.
@ -118,6 +145,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
if (!LoadedBeatmapSuccessfully)
return;
adjustLeaderboardPosition(); adjustLeaderboardPosition();
} }
@ -125,7 +156,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{ {
const float padding = 44; // enough margin to avoid the hit error display. const float padding = 44; // enough margin to avoid the hit error display.
leaderboard.Position = new Vector2(padding, padding + HUDOverlay.TopScoringElementsHeight); leaderboardFlow.Position = new Vector2(padding, padding + HUDOverlay.TopScoringElementsHeight);
} }
private void onMatchStarted() => Scheduler.Add(() => private void onMatchStarted() => Scheduler.Add(() =>

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
@ -42,6 +43,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
private ModDisplay userModsDisplay; private ModDisplay userModsDisplay;
private StateDisplay userStateDisplay; private StateDisplay userStateDisplay;
private IconButton kickButton;
public ParticipantPanel(MultiplayerRoomUser user) public ParticipantPanel(MultiplayerRoomUser user)
{ {
User = user; User = user;
@ -64,7 +67,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
{ {
new Dimension(GridSizeMode.Absolute, 18), new Dimension(GridSizeMode.Absolute, 18),
new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.AutoSize),
new Dimension() new Dimension(),
new Dimension(GridSizeMode.AutoSize),
}, },
Content = new[] Content = new[]
{ {
@ -157,7 +161,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
Margin = new MarginPadding { Right = 10 }, Margin = new MarginPadding { Right = 10 },
} }
} }
} },
kickButton = new KickButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0,
Margin = new MarginPadding(4),
Action = () =>
{
Debug.Assert(user != null);
Client.KickUser(user.Id);
}
},
}, },
} }
}; };
@ -167,7 +184,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
{ {
base.OnRoomUpdated(); base.OnRoomUpdated();
if (Room == null) if (Room == null || Client.LocalUser == null)
return; return;
const double fade_time = 50; const double fade_time = 50;
@ -179,6 +196,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability); userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability);
if (Client.IsHost && !User.Equals(Client.LocalUser))
kickButton.FadeIn(fade_time);
else
kickButton.FadeOut(fade_time);
if (Room.Host?.Equals(User) == true) if (Room.Host?.Equals(User) == true)
crown.FadeIn(fade_time); crown.FadeIn(fade_time);
else else
@ -211,13 +233,36 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
new OsuMenuItem("Give host", MenuItemType.Standard, () => new OsuMenuItem("Give host", MenuItemType.Standard, () =>
{ {
// Ensure the local user is still host. // Ensure the local user is still host.
if (Room.Host?.UserID != api.LocalUser.Value.Id) if (!Client.IsHost)
return; return;
Client.TransferHost(targetUser); Client.TransferHost(targetUser);
}),
new OsuMenuItem("Kick", MenuItemType.Destructive, () =>
{
// Ensure the local user is still host.
if (!Client.IsHost)
return;
Client.KickUser(targetUser);
}) })
}; };
} }
} }
public class KickButton : IconButton
{
public KickButton()
{
Icon = FontAwesome.Solid.UserTimes;
TooltipText = "Kick";
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
IconHoverColour = colours.Red;
}
}
} }
} }

View File

@ -4,6 +4,7 @@
using System; using System;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
@ -11,8 +12,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{ {
public class MultiSpectatorLeaderboard : MultiplayerGameplayLeaderboard public class MultiSpectatorLeaderboard : MultiplayerGameplayLeaderboard
{ {
public MultiSpectatorLeaderboard([NotNull] ScoreProcessor scoreProcessor, int[] userIds) public MultiSpectatorLeaderboard([NotNull] ScoreProcessor scoreProcessor, MultiplayerRoomUser[] users)
: base(scoreProcessor, userIds) : base(scoreProcessor, users)
{ {
} }
@ -32,7 +33,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
((SpectatingTrackedUserData)data).Clock = null; ((SpectatingTrackedUserData)data).Clock = null;
} }
protected override TrackedUserData CreateUserData(int userId, ScoreProcessor scoreProcessor) => new SpectatingTrackedUserData(userId, scoreProcessor); protected override TrackedUserData CreateUserData(MultiplayerRoomUser user, ScoreProcessor scoreProcessor) => new SpectatingTrackedUserData(user, scoreProcessor);
protected override void Update() protected override void Update()
{ {
@ -47,8 +48,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
[CanBeNull] [CanBeNull]
public IClock Clock; public IClock Clock;
public SpectatingTrackedUserData(int userId, ScoreProcessor scoreProcessor) public SpectatingTrackedUserData(MultiplayerRoomUser user, ScoreProcessor scoreProcessor)
: base(userId, scoreProcessor) : base(user, scoreProcessor)
{ {
} }

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Spectator; using osu.Game.Online.Spectator;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Spectate; using osu.Game.Screens.Spectate;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
@ -45,20 +46,26 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
private PlayerArea currentAudioSource; private PlayerArea currentAudioSource;
private bool canStartMasterClock; private bool canStartMasterClock;
private readonly MultiplayerRoomUser[] users;
/// <summary> /// <summary>
/// Creates a new <see cref="MultiSpectatorScreen"/>. /// Creates a new <see cref="MultiSpectatorScreen"/>.
/// </summary> /// </summary>
/// <param name="userIds">The players to spectate.</param> /// <param name="users">The players to spectate.</param>
public MultiSpectatorScreen(int[] userIds) public MultiSpectatorScreen(MultiplayerRoomUser[] users)
: base(userIds.Take(PlayerGrid.MAX_PLAYERS).ToArray()) : base(users.Select(u => u.UserID).ToArray())
{ {
instances = new PlayerArea[UserIds.Count]; this.users = users;
instances = new PlayerArea[Users.Count];
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Container leaderboardContainer; Container leaderboardContainer;
Container scoreDisplayContainer;
masterClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0); masterClockContainer = new MasterGameplayClockContainer(Beatmap.Value, 0);
InternalChildren = new[] InternalChildren = new[]
@ -67,28 +74,44 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
masterClockContainer.WithChild(new GridContainer masterClockContainer.WithChild(new GridContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[] RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
{
new Dimension(GridSizeMode.AutoSize)
},
Content = new[] Content = new[]
{ {
new Drawable[] new Drawable[]
{ {
leaderboardContainer = new Container scoreDisplayContainer = new Container
{ {
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.X AutoSizeAxes = Axes.Y
}, },
grid = new PlayerGrid { RelativeSizeAxes = Axes.Both } },
new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
Content = new[]
{
new Drawable[]
{
leaderboardContainer = new Container
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X
},
grid = new PlayerGrid { RelativeSizeAxes = Axes.Both }
}
}
}
} }
} }
}) })
}; };
for (int i = 0; i < UserIds.Count; i++) for (int i = 0; i < Users.Count; i++)
{ {
grid.Add(instances[i] = new PlayerArea(UserIds[i], masterClockContainer.GameplayClock)); grid.Add(instances[i] = new PlayerArea(Users[i], masterClockContainer.GameplayClock));
syncManager.AddPlayerClock(instances[i].GameplayClock); syncManager.AddPlayerClock(instances[i].GameplayClock);
} }
@ -97,7 +120,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
var scoreProcessor = Ruleset.Value.CreateInstance().CreateScoreProcessor(); var scoreProcessor = Ruleset.Value.CreateInstance().CreateScoreProcessor();
scoreProcessor.ApplyBeatmap(playableBeatmap); scoreProcessor.ApplyBeatmap(playableBeatmap);
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, UserIds.ToArray()) LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, users)
{ {
Expanded = { Value = true }, Expanded = { Value = true },
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
@ -108,6 +131,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
leaderboard.AddClock(instance.UserId, instance.GameplayClock); leaderboard.AddClock(instance.UserId, instance.GameplayClock);
leaderboardContainer.Add(leaderboard); leaderboardContainer.Add(leaderboard);
if (leaderboard.TeamScores.Count == 2)
{
LoadComponentAsync(new MatchScoreDisplay
{
Team1Score = { BindTarget = leaderboard.TeamScores.First().Value },
Team2Score = { BindTarget = leaderboard.TeamScores.Last().Value },
}, scoreDisplayContainer.Add);
}
}); });
} }

View File

@ -48,10 +48,9 @@ namespace osu.Game.Screens.Play.HUD
/// </param> /// </param>
public ILeaderboardScore AddPlayer([CanBeNull] User user, bool isTracked) public ILeaderboardScore AddPlayer([CanBeNull] User user, bool isTracked)
{ {
var drawable = new GameplayLeaderboardScore(user, isTracked) var drawable = CreateLeaderboardScoreDrawable(user, isTracked);
{
Expanded = { BindTarget = Expanded }, drawable.Expanded.BindTo(Expanded);
};
base.Add(drawable); base.Add(drawable);
drawable.TotalScore.BindValueChanged(_ => sorting.Invalidate(), true); drawable.TotalScore.BindValueChanged(_ => sorting.Invalidate(), true);
@ -61,6 +60,9 @@ namespace osu.Game.Screens.Play.HUD
return drawable; return drawable;
} }
protected virtual GameplayLeaderboardScore CreateLeaderboardScoreDrawable(User user, bool isTracked) =>
new GameplayLeaderboardScore(user, isTracked);
public sealed override void Add(GameplayLeaderboardScore drawable) public sealed override void Add(GameplayLeaderboardScore drawable)
{ {
throw new NotSupportedException($"Use {nameof(AddPlayer)} instead."); throw new NotSupportedException($"Use {nameof(AddPlayer)} instead.");

View File

@ -54,6 +54,10 @@ namespace osu.Game.Screens.Play.HUD
public BindableInt Combo { get; } = new BindableInt(); public BindableInt Combo { get; } = new BindableInt();
public BindableBool HasQuit { get; } = new BindableBool(); public BindableBool HasQuit { get; } = new BindableBool();
public Color4? BackgroundColour { get; set; }
public Color4? TextColour { get; set; }
private int? scorePosition; private int? scorePosition;
public int? ScorePosition public int? ScorePosition
@ -331,19 +335,19 @@ namespace osu.Game.Screens.Play.HUD
if (scorePosition == 1) if (scorePosition == 1)
{ {
widthExtension = true; widthExtension = true;
panelColour = Color4Extensions.FromHex("7fcc33"); panelColour = BackgroundColour ?? Color4Extensions.FromHex("7fcc33");
textColour = Color4.White; textColour = TextColour ?? Color4.White;
} }
else if (trackedPlayer) else if (trackedPlayer)
{ {
widthExtension = true; widthExtension = true;
panelColour = Color4Extensions.FromHex("ffd966"); panelColour = BackgroundColour ?? Color4Extensions.FromHex("ffd966");
textColour = Color4Extensions.FromHex("2e576b"); textColour = TextColour ?? Color4Extensions.FromHex("2e576b");
} }
else else
{ {
panelColour = Color4Extensions.FromHex("3399cc"); panelColour = BackgroundColour ?? Color4Extensions.FromHex("3399cc");
textColour = Color4.White; textColour = TextColour ?? Color4.White;
} }
this.TransformTo(nameof(SizeContainerLeftPadding), widthExtension ? -top_player_left_width_extension : 0, panel_transition_duration, Easing.OutElastic); this.TransformTo(nameof(SizeContainerLeftPadding), widthExtension ? -top_player_left_width_extension : 0, panel_transition_duration, Easing.OutElastic);

View File

@ -0,0 +1,176 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osuTK;
namespace osu.Game.Screens.Play.HUD
{
public class MatchScoreDisplay : CompositeDrawable
{
private const float bar_height = 18;
private const float font_size = 50;
public BindableInt Team1Score = new BindableInt();
public BindableInt Team2Score = new BindableInt();
protected MatchScoreCounter Score1Text;
protected MatchScoreCounter Score2Text;
private Drawable score1Bar;
private Drawable score2Bar;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChildren = new[]
{
new Box
{
Name = "top bar red (static)",
RelativeSizeAxes = Axes.X,
Height = bar_height / 4,
Width = 0.5f,
Colour = colours.TeamColourRed,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopRight
},
new Box
{
Name = "top bar blue (static)",
RelativeSizeAxes = Axes.X,
Height = bar_height / 4,
Width = 0.5f,
Colour = colours.TeamColourBlue,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopLeft
},
score1Bar = new Box
{
Name = "top bar red",
RelativeSizeAxes = Axes.X,
Height = bar_height,
Width = 0,
Colour = colours.TeamColourRed,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopRight
},
score2Bar = new Box
{
Name = "top bar blue",
RelativeSizeAxes = Axes.X,
Height = bar_height,
Width = 0,
Colour = colours.TeamColourBlue,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopLeft
},
new Container
{
RelativeSizeAxes = Axes.X,
Height = font_size + bar_height,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Children = new Drawable[]
{
Score1Text = new MatchScoreCounter
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre
},
Score2Text = new MatchScoreCounter
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre
},
}
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Team1Score.BindValueChanged(_ => updateScores());
Team2Score.BindValueChanged(_ => updateScores());
}
private void updateScores()
{
Score1Text.Current.Value = Team1Score.Value;
Score2Text.Current.Value = Team2Score.Value;
int comparison = Team1Score.Value.CompareTo(Team2Score.Value);
if (comparison > 0)
{
Score1Text.Winning = true;
Score2Text.Winning = false;
}
else if (comparison < 0)
{
Score1Text.Winning = false;
Score2Text.Winning = true;
}
else
{
Score1Text.Winning = false;
Score2Text.Winning = false;
}
var winningBar = Team1Score.Value > Team2Score.Value ? score1Bar : score2Bar;
var losingBar = Team1Score.Value <= Team2Score.Value ? score1Bar : score2Bar;
var diff = Math.Max(Team1Score.Value, Team2Score.Value) - Math.Min(Team1Score.Value, Team2Score.Value);
losingBar.ResizeWidthTo(0, 400, Easing.OutQuint);
winningBar.ResizeWidthTo(Math.Min(0.4f, MathF.Pow(diff / 1500000f, 0.5f) / 2), 400, Easing.OutQuint);
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
Score1Text.X = -Math.Max(5 + Score1Text.DrawWidth / 2, score1Bar.DrawWidth);
Score2Text.X = Math.Max(5 + Score2Text.DrawWidth / 2, score2Bar.DrawWidth);
}
protected class MatchScoreCounter : ScoreCounter
{
private OsuSpriteText displayedSpriteText;
public MatchScoreCounter()
{
Margin = new MarginPadding { Top = bar_height, Horizontal = 10 };
}
public bool Winning
{
set => updateFont(value);
}
protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s =>
{
displayedSpriteText = s;
displayedSpriteText.Spacing = new Vector2(-6);
updateFont(false);
});
private void updateFont(bool winning)
=> displayedSpriteText.Font = winning
? OsuFont.Torus.With(weight: FontWeight.Bold, size: font_size, fixedWidth: true)
: OsuFont.Torus.With(weight: FontWeight.Regular, size: font_size * 0.8f, fixedWidth: true);
}
}
}

View File

@ -7,12 +7,17 @@ using System.Collections.Specialized;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Online.Spectator; using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Users;
using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD namespace osu.Game.Screens.Play.HUD
{ {
@ -21,6 +26,11 @@ namespace osu.Game.Screens.Play.HUD
{ {
protected readonly Dictionary<int, TrackedUserData> UserScores = new Dictionary<int, TrackedUserData>(); protected readonly Dictionary<int, TrackedUserData> UserScores = new Dictionary<int, TrackedUserData>();
public readonly SortedDictionary<int, BindableInt> TeamScores = new SortedDictionary<int, BindableInt>();
[Resolved]
private OsuColour colours { get; set; }
[Resolved] [Resolved]
private SpectatorClient spectatorClient { get; set; } private SpectatorClient spectatorClient { get; set; }
@ -31,21 +41,24 @@ namespace osu.Game.Screens.Play.HUD
private UserLookupCache userLookupCache { get; set; } private UserLookupCache userLookupCache { get; set; }
private readonly ScoreProcessor scoreProcessor; private readonly ScoreProcessor scoreProcessor;
private readonly IBindableList<int> playingUsers; private readonly MultiplayerRoomUser[] playingUsers;
private Bindable<ScoringMode> scoringMode; private Bindable<ScoringMode> scoringMode;
private readonly IBindableList<int> playingUserIds = new BindableList<int>();
private bool hasTeams => TeamScores.Count > 0;
/// <summary> /// <summary>
/// Construct a new leaderboard. /// Construct a new leaderboard.
/// </summary> /// </summary>
/// <param name="scoreProcessor">A score processor instance to handle score calculation for scores of users in the match.</param> /// <param name="scoreProcessor">A score processor instance to handle score calculation for scores of users in the match.</param>
/// <param name="userIds">IDs of all users in this match.</param> /// <param name="users">IDs of all users in this match.</param>
public MultiplayerGameplayLeaderboard(ScoreProcessor scoreProcessor, int[] userIds) public MultiplayerGameplayLeaderboard(ScoreProcessor scoreProcessor, MultiplayerRoomUser[] users)
{ {
// todo: this will eventually need to be created per user to support different mod combinations. // todo: this will eventually need to be created per user to support different mod combinations.
this.scoreProcessor = scoreProcessor; this.scoreProcessor = scoreProcessor;
// todo: this will likely be passed in as User instances. playingUsers = users;
playingUsers = new BindableList<int>(userIds);
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -53,14 +66,17 @@ namespace osu.Game.Screens.Play.HUD
{ {
scoringMode = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode); scoringMode = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode);
foreach (var userId in playingUsers) foreach (var user in playingUsers)
{ {
var trackedUser = CreateUserData(userId, scoreProcessor); var trackedUser = CreateUserData(user, scoreProcessor);
trackedUser.ScoringMode.BindTo(scoringMode); trackedUser.ScoringMode.BindTo(scoringMode);
UserScores[userId] = trackedUser; UserScores[user.UserID] = trackedUser;
if (trackedUser.Team is int team && !TeamScores.ContainsKey(team))
TeamScores.Add(team, new BindableInt());
} }
userLookupCache.GetUsersAsync(playingUsers.ToArray()).ContinueWith(users => Schedule(() => userLookupCache.GetUsersAsync(playingUsers.Select(u => u.UserID).ToArray()).ContinueWith(users => Schedule(() =>
{ {
foreach (var user in users.Result) foreach (var user in users.Result)
{ {
@ -83,23 +99,50 @@ namespace osu.Game.Screens.Play.HUD
base.LoadComplete(); base.LoadComplete();
// BindableList handles binding in a really bad way (Clear then AddRange) so we need to do this manually.. // BindableList handles binding in a really bad way (Clear then AddRange) so we need to do this manually..
foreach (int userId in playingUsers) foreach (var user in playingUsers)
{ {
spectatorClient.WatchUser(userId); spectatorClient.WatchUser(user.UserID);
if (!multiplayerClient.CurrentMatchPlayingUserIds.Contains(userId)) if (!multiplayerClient.CurrentMatchPlayingUserIds.Contains(user.UserID))
usersChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { userId })); usersChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { user.UserID }));
} }
// bind here is to support players leaving the match. // bind here is to support players leaving the match.
// new players are not supported. // new players are not supported.
playingUsers.BindTo(multiplayerClient.CurrentMatchPlayingUserIds); playingUserIds.BindTo(multiplayerClient.CurrentMatchPlayingUserIds);
playingUsers.BindCollectionChanged(usersChanged); playingUserIds.BindCollectionChanged(usersChanged);
// this leaderboard should be guaranteed to be completely loaded before the gameplay starts (is a prerequisite in MultiplayerPlayer). // this leaderboard should be guaranteed to be completely loaded before the gameplay starts (is a prerequisite in MultiplayerPlayer).
spectatorClient.OnNewFrames += handleIncomingFrames; spectatorClient.OnNewFrames += handleIncomingFrames;
} }
protected virtual TrackedUserData CreateUserData(MultiplayerRoomUser user, ScoreProcessor scoreProcessor) => new TrackedUserData(user, scoreProcessor);
protected override GameplayLeaderboardScore CreateLeaderboardScoreDrawable(User user, bool isTracked)
{
var leaderboardScore = base.CreateLeaderboardScoreDrawable(user, isTracked);
if (UserScores[user.Id].Team is int team)
{
leaderboardScore.BackgroundColour = getTeamColour(team).Lighten(1.2f);
leaderboardScore.TextColour = Color4.White;
}
return leaderboardScore;
}
private Color4 getTeamColour(int team)
{
switch (team)
{
case 0:
return colours.TeamColourRed;
default:
return colours.TeamColourBlue;
}
}
private void usersChanged(object sender, NotifyCollectionChangedEventArgs e) private void usersChanged(object sender, NotifyCollectionChangedEventArgs e)
{ {
switch (e.Action) switch (e.Action)
@ -124,9 +167,26 @@ namespace osu.Game.Screens.Play.HUD
trackedData.Frames.Add(new TimedFrame(bundle.Frames.First().Time, bundle.Header)); trackedData.Frames.Add(new TimedFrame(bundle.Frames.First().Time, bundle.Header));
trackedData.UpdateScore(); trackedData.UpdateScore();
updateTotals();
}); });
protected virtual TrackedUserData CreateUserData(int userId, ScoreProcessor scoreProcessor) => new TrackedUserData(userId, scoreProcessor); private void updateTotals()
{
if (!hasTeams)
return;
foreach (var scores in TeamScores.Values) scores.Value = 0;
foreach (var u in UserScores.Values)
{
if (u.Team == null)
continue;
if (TeamScores.TryGetValue(u.Team.Value, out var team))
team.Value += (int)u.Score.Value;
}
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
@ -136,7 +196,7 @@ namespace osu.Game.Screens.Play.HUD
{ {
foreach (var user in playingUsers) foreach (var user in playingUsers)
{ {
spectatorClient.StopWatchingUser(user); spectatorClient.StopWatchingUser(user.UserID);
} }
spectatorClient.OnNewFrames -= handleIncomingFrames; spectatorClient.OnNewFrames -= handleIncomingFrames;
@ -145,7 +205,7 @@ namespace osu.Game.Screens.Play.HUD
protected class TrackedUserData protected class TrackedUserData
{ {
public readonly int UserId; public readonly MultiplayerRoomUser User;
public readonly ScoreProcessor ScoreProcessor; public readonly ScoreProcessor ScoreProcessor;
public readonly BindableDouble Score = new BindableDouble(); public readonly BindableDouble Score = new BindableDouble();
@ -157,9 +217,11 @@ namespace osu.Game.Screens.Play.HUD
public readonly List<TimedFrame> Frames = new List<TimedFrame>(); public readonly List<TimedFrame> Frames = new List<TimedFrame>();
public TrackedUserData(int userId, ScoreProcessor scoreProcessor) public int? Team => (User.MatchState as TeamVersusUserState)?.TeamID;
public TrackedUserData(MultiplayerRoomUser user, ScoreProcessor scoreProcessor)
{ {
UserId = userId; User = user;
ScoreProcessor = scoreProcessor; ScoreProcessor = scoreProcessor;
ScoringMode.BindValueChanged(_ => UpdateScore()); ScoringMode.BindValueChanged(_ => UpdateScore());

View File

@ -24,9 +24,9 @@ namespace osu.Game.Screens.Spectate
/// </summary> /// </summary>
public abstract class SpectatorScreen : OsuScreen public abstract class SpectatorScreen : OsuScreen
{ {
protected IReadOnlyList<int> UserIds => userIds; protected IReadOnlyList<int> Users => users;
private readonly List<int> userIds = new List<int>(); private readonly List<int> users = new List<int>();
[Resolved] [Resolved]
private BeatmapManager beatmaps { get; set; } private BeatmapManager beatmaps { get; set; }
@ -50,17 +50,17 @@ namespace osu.Game.Screens.Spectate
/// <summary> /// <summary>
/// Creates a new <see cref="SpectatorScreen"/>. /// Creates a new <see cref="SpectatorScreen"/>.
/// </summary> /// </summary>
/// <param name="userIds">The users to spectate.</param> /// <param name="users">The users to spectate.</param>
protected SpectatorScreen(params int[] userIds) protected SpectatorScreen(params int[] users)
{ {
this.userIds.AddRange(userIds); this.users.AddRange(users);
} }
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
userLookupCache.GetUsersAsync(userIds.ToArray()).ContinueWith(users => Schedule(() => userLookupCache.GetUsersAsync(users.ToArray()).ContinueWith(users => Schedule(() =>
{ {
foreach (var u in users.Result) foreach (var u in users.Result)
{ {
@ -207,7 +207,7 @@ namespace osu.Game.Screens.Spectate
{ {
onUserStateRemoved(userId); onUserStateRemoved(userId);
userIds.Remove(userId); users.Remove(userId);
userMap.Remove(userId); userMap.Remove(userId);
spectatorClient.StopWatchingUser(userId); spectatorClient.StopWatchingUser(userId);

View File

@ -36,24 +36,29 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
if (joinRoom) if (joinRoom)
{ {
var room = new Room var room = CreateRoom();
{
Name = { Value = "test name" },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo },
Ruleset = { Value = Ruleset.Value }
}
}
};
RoomManager.CreateRoom(room); RoomManager.CreateRoom(room);
SelectedRoom.Value = room; SelectedRoom.Value = room;
} }
}); });
protected virtual Room CreateRoom()
{
return new Room
{
Name = { Value = "test name" },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo },
Ruleset = { Value = Ruleset.Value }
}
}
};
}
public override void SetUpSteps() public override void SetUpSteps()
{ {
base.SetUpSteps(); base.SetUpSteps();

View File

@ -174,6 +174,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
public override Task TransferHost(int userId) => ((IMultiplayerClient)this).HostChanged(userId); public override Task TransferHost(int userId) => ((IMultiplayerClient)this).HostChanged(userId);
public override Task KickUser(int userId)
{
Debug.Assert(Room != null);
return ((IMultiplayerClient)this).UserLeft(Room.Users.Single(u => u.UserID == userId));
}
public override async Task ChangeSettings(MultiplayerRoomSettings settings) public override async Task ChangeSettings(MultiplayerRoomSettings settings)
{ {
Debug.Assert(Room != null); Debug.Assert(Room != null);

View File

@ -36,7 +36,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="10.3.0" /> <PackageReference Include="Realm" Version="10.3.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.810.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.811.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.810.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.810.0" />
<PackageReference Include="Sentry" Version="3.8.3" /> <PackageReference Include="Sentry" Version="3.8.3" />
<PackageReference Include="SharpCompress" Version="0.28.3" /> <PackageReference Include="SharpCompress" Version="0.28.3" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.810.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.811.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.810.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.810.0" />
</ItemGroup> </ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) --> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@ -93,7 +93,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2021.810.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.811.0" />
<PackageReference Include="SharpCompress" Version="0.28.3" /> <PackageReference Include="SharpCompress" Version="0.28.3" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />