1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-15 15:27:20 +08:00

Merge branch 'master' into catch-legacy-explosions-v2

This commit is contained in:
Dan Balasescu 2021-08-13 14:12:02 +09:00 committed by GitHub
commit 066473393c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 2661 additions and 1100 deletions

View File

@ -41,6 +41,11 @@ namespace osu.Game.Rulesets.Osu.Edit
protected override GameplayCursorContainer CreateCursor() => null;
public OsuEditorPlayfield()
{
HitPolicy = new AnyOrderHitPolicy();
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{

View File

@ -0,0 +1,22 @@
// 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.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.UI
{
/// <summary>
/// An <see cref="IHitPolicy"/> which allows hitobjects to be hit in any order.
/// </summary>
public class AnyOrderHitPolicy : IHitPolicy
{
public IHitObjectContainer HitObjectContainer { get; set; }
public bool IsHittable(DrawableHitObject hitObject, double time) => true;
public void HandleHit(DrawableHitObject hitObject)
{
}
}
}

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Logging;
using osu.Framework.Testing;
using osu.Game.Graphics.Sprites;
using osu.Game.Online;
using osuTK;
@ -15,6 +16,7 @@ using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Components
{
[HeadlessTest]
public class TestScenePollingComponent : OsuTestScene
{
private Container pollBox;

View File

@ -0,0 +1,168 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Overlays;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Tests.Beatmaps;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneDrawableRoom : OsuTestScene
{
[Cached]
private readonly Bindable<Room> selectedRoom = new Bindable<Room>();
[Cached]
protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
[Test]
public void TestMultipleStatuses()
{
AddStep("create rooms", () =>
{
Child = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.9f),
Spacing = new Vector2(10),
Children = new Drawable[]
{
createDrawableRoom(new Room
{
Name = { Value = "Flyte's Trash Playlist" },
Status = { Value = new RoomStatusOpen() },
EndDate = { Value = DateTimeOffset.Now.AddDays(1) },
Playlist =
{
new PlaylistItem
{
Beatmap =
{
Value = new TestBeatmap(new OsuRuleset().RulesetInfo)
{
BeatmapInfo =
{
StarDifficulty = 2.5
}
}.BeatmapInfo,
}
}
}
}),
createDrawableRoom(new Room
{
Name = { Value = "Room 2" },
Status = { Value = new RoomStatusPlaying() },
EndDate = { Value = DateTimeOffset.Now.AddDays(1) },
Playlist =
{
new PlaylistItem
{
Beatmap =
{
Value = new TestBeatmap(new OsuRuleset().RulesetInfo)
{
BeatmapInfo =
{
StarDifficulty = 2.5
}
}.BeatmapInfo,
}
},
new PlaylistItem
{
Beatmap =
{
Value = new TestBeatmap(new OsuRuleset().RulesetInfo)
{
BeatmapInfo =
{
StarDifficulty = 4.5
}
}.BeatmapInfo,
}
}
}
}),
createDrawableRoom(new Room
{
Name = { Value = "Room 3" },
Status = { Value = new RoomStatusEnded() },
EndDate = { Value = DateTimeOffset.Now },
}),
createDrawableRoom(new Room
{
Name = { Value = "Room 4 (realtime)" },
Status = { Value = new RoomStatusOpen() },
Category = { Value = RoomCategory.Realtime },
}),
createDrawableRoom(new Room
{
Name = { Value = "Room 4 (spotlight)" },
Status = { Value = new RoomStatusOpen() },
Category = { Value = RoomCategory.Spotlight },
}),
}
};
});
}
[Test]
public void TestEnableAndDisablePassword()
{
DrawableRoom drawableRoom = null;
Room room = null;
AddStep("create room", () => Child = drawableRoom = createDrawableRoom(room = new Room
{
Name = { Value = "Room with password" },
Status = { Value = new RoomStatusOpen() },
Category = { Value = RoomCategory.Realtime },
}));
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
AddStep("set password", () => room.Password.Value = "password");
AddAssert("password icon visible", () => Precision.AlmostEquals(1, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
AddStep("unset password", () => room.Password.Value = string.Empty);
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
}
private DrawableRoom createDrawableRoom(Room room)
{
room.Host.Value ??= new User { Username = "peppy", Id = 2 };
if (room.RecentParticipants.Count == 0)
{
room.RecentParticipants.AddRange(Enumerable.Range(0, 20).Select(i => new User
{
Id = i,
Username = $"User {i}"
}));
}
var drawableRoom = new DrawableRoom(room) { MatchingFilter = true };
drawableRoom.Action = () => drawableRoom.State = drawableRoom.State == SelectionState.Selected ? SelectionState.NotSelected : SelectionState.Selected;
return drawableRoom;
}
}
}

View File

@ -1,49 +0,0 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneLoungeRoomInfo : OnlinePlayTestScene
{
[SetUp]
public new void Setup() => Schedule(() =>
{
SelectedRoom.Value = new Room();
Child = new RoomInfo
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 500
};
});
[Test]
public void TestNonSelectedRoom()
{
AddStep("set null room", () => SelectedRoom.Value.RoomID.Value = null);
}
[Test]
public void TestOpenRoom()
{
AddStep("set open room", () =>
{
SelectedRoom.Value.RoomID.Value = 0;
SelectedRoom.Value.Name.Value = "Room 0";
SelectedRoom.Value.Host.Value = new User { Username = "peppy", Id = 2 };
SelectedRoom.Value.EndDate.Value = DateTimeOffset.Now.AddMonths(1);
SelectedRoom.Value.Status.Value = new RoomStatusOpen();
});
}
}
}

View File

@ -6,9 +6,11 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play.HUD;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
@ -31,7 +33,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
};
foreach (var (userId, _) in clocks)
{
SpectatorClient.StartPlay(userId, 0);
OnlinePlayDependencies.Client.AddUser(new User { Id = userId });
}
});
AddStep("create leaderboard", () =>
@ -41,7 +46,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
var scoreProcessor = new OsuScoreProcessor();
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);

View File

@ -8,6 +8,8 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play;
@ -26,7 +28,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private MultiSpectatorScreen spectatorScreen;
private readonly List<int> playingUserIds = new List<int>();
private readonly List<MultiplayerRoomUser> playingUsers = new List<MultiplayerRoomUser>();
private BeatmapSetInfo importedSet;
private BeatmapInfo importedBeatmap;
@ -41,7 +43,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
[SetUp]
public new void Setup() => Schedule(() => playingUserIds.Clear());
public new void Setup() => Schedule(() => playingUsers.Clear());
[Test]
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_2_ID }, true);
playingUserIds.Add(PLAYER_1_ID);
playingUserIds.Add(PLAYER_2_ID);
playingUsers.Add(new MultiplayerRoomUser(PLAYER_1_ID));
playingUsers.Add(new MultiplayerRoomUser(PLAYER_2_ID));
});
loadSpectateScreen(false);
@ -78,6 +80,38 @@ namespace osu.Game.Tests.Visual.Multiplayer
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]
public void TestTimeDoesNotProgressWhileAllPlayersPaused()
{
@ -254,7 +288,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Beatmap.Value = beatmapManager.GetWorkingBeatmap(importedBeatmap);
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));
@ -269,7 +303,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
OnlinePlayDependencies.Client.AddUser(new User { Id = id }, true);
SpectatorClient.StartPlay(id, beatmapId ?? importedBeatmapId);
playingUserIds.Add(id);
playingUsers.Add(new MultiplayerRoomUser(id));
}
});
}

View File

@ -423,10 +423,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void createRoom(Func<Room> room)
{
AddStep("open room", () =>
{
multiplayerScreen.OpenNewRoom(room());
});
AddUntilStep("wait for lounge", () => multiplayerScreen.ChildrenOfType<LoungeSubScreen>().SingleOrDefault()?.IsLoaded == true);
AddStep("open room", () => multiplayerScreen.ChildrenOfType<LoungeSubScreen>().Single().Open(room()));
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddWaitStep("wait for transition", 2);

View File

@ -12,6 +12,7 @@ using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Osu.Scoring;
@ -51,12 +52,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
OsuScoreProcessor scoreProcessor;
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)
{
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[]
@ -64,9 +66,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
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,
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);
}
[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]
public void TestManyUsers()
{

View File

@ -0,0 +1,95 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneRankRangePill : MultiplayerTestScene
{
[SetUp]
public new void Setup() => Schedule(() =>
{
Child = new RankRangePill
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
};
});
[Test]
public void TestSingleUser()
{
AddStep("add user", () =>
{
Client.AddUser(new User
{
Id = 2,
Statistics = { GlobalRank = 1234 }
});
// Remove the local user so only the one above is displayed.
Client.RemoveUser(API.LocalUser.Value);
});
}
[Test]
public void TestMultipleUsers()
{
AddStep("add users", () =>
{
Client.AddUser(new User
{
Id = 2,
Statistics = { GlobalRank = 1234 }
});
Client.AddUser(new User
{
Id = 3,
Statistics = { GlobalRank = 3333 }
});
Client.AddUser(new User
{
Id = 4,
Statistics = { GlobalRank = 4321 }
});
// Remove the local user so only the ones above are displayed.
Client.RemoveUser(API.LocalUser.Value);
});
}
[TestCase(1, 10)]
[TestCase(10, 100)]
[TestCase(100, 1000)]
[TestCase(1000, 10000)]
[TestCase(10000, 100000)]
[TestCase(100000, 1000000)]
[TestCase(1000000, 10000000)]
public void TestRange(int min, int max)
{
AddStep("add users", () =>
{
Client.AddUser(new User
{
Id = 2,
Statistics = { GlobalRank = min }
});
Client.AddUser(new User
{
Id = 3,
Statistics = { GlobalRank = max }
});
// Remove the local user so only the ones above are displayed.
Client.RemoveUser(API.LocalUser.Value);
});
}
}
}

View File

@ -0,0 +1,143 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Tests.Visual.OnlinePlay;
using osu.Game.Users;
using osu.Game.Users.Drawables;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneRecentParticipantsList : OnlinePlayTestScene
{
private RecentParticipantsList list;
[SetUp]
public new void Setup() => Schedule(() =>
{
SelectedRoom.Value = new Room { Name = { Value = "test room" } };
Child = list = new RecentParticipantsList
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
NumberOfCircles = 4
};
});
[Test]
public void TestCircleCountNearLimit()
{
AddStep("add 8 users", () =>
{
for (int i = 0; i < 8; i++)
addUser(i);
});
AddStep("set 8 circles", () => list.NumberOfCircles = 8);
AddAssert("0 hidden users", () => list.ChildrenOfType<RecentParticipantsList.HiddenUserCount>().Single().Count == 0);
AddStep("add one more user", () => addUser(9));
AddAssert("2 hidden users", () => list.ChildrenOfType<RecentParticipantsList.HiddenUserCount>().Single().Count == 2);
AddStep("remove first user", () => removeUserAt(0));
AddAssert("0 hidden users", () => list.ChildrenOfType<RecentParticipantsList.HiddenUserCount>().Single().Count == 0);
AddStep("add one more user", () => addUser(9));
AddAssert("2 hidden users", () => list.ChildrenOfType<RecentParticipantsList.HiddenUserCount>().Single().Count == 2);
AddStep("remove last user", () => removeUserAt(8));
AddAssert("0 hidden users", () => list.ChildrenOfType<RecentParticipantsList.HiddenUserCount>().Single().Count == 0);
}
[Test]
public void TestHiddenUsersBecomeDisplayed()
{
AddStep("add 8 users", () =>
{
for (int i = 0; i < 8; i++)
addUser(i);
});
AddStep("set 3 circles", () => list.NumberOfCircles = 3);
for (int i = 0; i < 8; i++)
{
AddStep("remove user", () => removeUserAt(0));
int remainingUsers = 7 - i;
int displayedUsers = remainingUsers > 3 ? 2 : remainingUsers;
AddAssert($"{displayedUsers} avatars displayed", () => list.ChildrenOfType<UpdateableAvatar>().Count() == displayedUsers);
}
}
[Test]
public void TestCircleCount()
{
AddStep("add 50 users", () =>
{
for (int i = 0; i < 50; i++)
addUser(i);
});
AddStep("set 3 circles", () => list.NumberOfCircles = 3);
AddAssert("2 users displayed", () => list.ChildrenOfType<UpdateableAvatar>().Count() == 2);
AddAssert("48 hidden users", () => list.ChildrenOfType<RecentParticipantsList.HiddenUserCount>().Single().Count == 48);
AddStep("set 10 circles", () => list.NumberOfCircles = 10);
AddAssert("9 users displayed", () => list.ChildrenOfType<UpdateableAvatar>().Count() == 9);
AddAssert("41 hidden users", () => list.ChildrenOfType<RecentParticipantsList.HiddenUserCount>().Single().Count == 41);
}
[Test]
public void TestAddAndRemoveUsers()
{
AddStep("add 50 users", () =>
{
for (int i = 0; i < 50; i++)
addUser(i);
});
AddStep("remove from start", () => removeUserAt(0));
AddAssert("3 circles displayed", () => list.ChildrenOfType<UpdateableAvatar>().Count() == 3);
AddAssert("46 hidden users", () => list.ChildrenOfType<RecentParticipantsList.HiddenUserCount>().Single().Count == 46);
AddStep("remove from end", () => removeUserAt(SelectedRoom.Value.RecentParticipants.Count - 1));
AddAssert("3 circles displayed", () => list.ChildrenOfType<UpdateableAvatar>().Count() == 3);
AddAssert("45 hidden users", () => list.ChildrenOfType<RecentParticipantsList.HiddenUserCount>().Single().Count == 45);
AddRepeatStep("remove 45 users", () => removeUserAt(0), 45);
AddAssert("3 circles displayed", () => list.ChildrenOfType<UpdateableAvatar>().Count() == 3);
AddAssert("0 hidden users", () => list.ChildrenOfType<RecentParticipantsList.HiddenUserCount>().Single().Count == 0);
AddAssert("hidden users bubble hidden", () => list.ChildrenOfType<RecentParticipantsList.HiddenUserCount>().Single().Alpha < 0.5f);
AddStep("remove another user", () => removeUserAt(0));
AddAssert("2 circles displayed", () => list.ChildrenOfType<UpdateableAvatar>().Count() == 2);
AddAssert("0 hidden users", () => list.ChildrenOfType<RecentParticipantsList.HiddenUserCount>().Single().Count == 0);
AddRepeatStep("remove the remaining two users", () => removeUserAt(0), 2);
AddAssert("0 circles displayed", () => !list.ChildrenOfType<UpdateableAvatar>().Any());
}
private void addUser(int id)
{
SelectedRoom.Value.RecentParticipants.Add(new User
{
Id = id,
Username = $"User {id}"
});
SelectedRoom.Value.ParticipantCount.Value++;
}
private void removeUserAt(int index)
{
SelectedRoom.Value.RecentParticipants.RemoveAt(index);
SelectedRoom.Value.ParticipantCount.Value--;
}
}
}

View File

@ -1,81 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneRoomStatus : OsuTestScene
{
[Test]
public void TestMultipleStatuses()
{
AddStep("create rooms", () =>
{
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Width = 0.5f,
Children = new Drawable[]
{
new DrawableRoom(new Room
{
Name = { Value = "Open - ending in 1 day" },
Status = { Value = new RoomStatusOpen() },
EndDate = { Value = DateTimeOffset.Now.AddDays(1) }
}) { MatchingFilter = true },
new DrawableRoom(new Room
{
Name = { Value = "Playing - ending in 1 day" },
Status = { Value = new RoomStatusPlaying() },
EndDate = { Value = DateTimeOffset.Now.AddDays(1) }
}) { MatchingFilter = true },
new DrawableRoom(new Room
{
Name = { Value = "Ended" },
Status = { Value = new RoomStatusEnded() },
EndDate = { Value = DateTimeOffset.Now }
}) { MatchingFilter = true },
new DrawableRoom(new Room
{
Name = { Value = "Open" },
Status = { Value = new RoomStatusOpen() },
Category = { Value = RoomCategory.Realtime }
}) { MatchingFilter = true },
}
};
});
}
[Test]
public void TestEnableAndDisablePassword()
{
DrawableRoom drawableRoom = null;
Room room = null;
AddStep("create room", () => Child = drawableRoom = new DrawableRoom(room = new Room
{
Name = { Value = "Room with password" },
Status = { Value = new RoomStatusOpen() },
Category = { Value = RoomCategory.Realtime },
}) { MatchingFilter = true });
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
AddStep("set password", () => room.Password.Value = "password");
AddAssert("password icon visible", () => Precision.AlmostEquals(1, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
AddStep("unset password", () => room.Password.Value = string.Empty);
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
}
}
}

View File

@ -17,6 +17,7 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
@ -150,10 +151,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void createRoom(Func<Room> room)
{
AddStep("open room", () =>
{
multiplayerScreen.OpenNewRoom(room());
});
AddStep("open room", () => multiplayerScreen.ChildrenOfType<LoungeSubScreen>().Single().Open(room()));
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
AddWaitStep("wait for transition", 2);

View File

@ -16,6 +16,7 @@ using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
@ -316,7 +317,8 @@ namespace osu.Game.Tests.Visual.Navigation
PushAndConfirm(() => multiplayer = new TestMultiplayer());
AddStep("open room", () => multiplayer.OpenNewRoom());
AddUntilStep("wait for lounge", () => multiplayer.ChildrenOfType<LoungeSubScreen>().SingleOrDefault()?.IsLoaded == true);
AddStep("open room", () => multiplayer.ChildrenOfType<LoungeSubScreen>().Single().Open());
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action());
AddWaitStep("wait two frames", 2);
}

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Linq;
using Humanizer;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
@ -95,9 +96,11 @@ namespace osu.Game.Tests.Visual.Online
AddAssert(@"no stream selected", () => changelog.Header.Streams.Current.Value == null);
}
[Test]
public void ShowWithBuild()
[TestCase(false)]
[TestCase(true)]
public void ShowWithBuild(bool isSupporter)
{
AddStep(@"set supporter", () => dummyAPI.LocalUser.Value.IsSupporter = isSupporter);
showBuild(() => new APIChangelogBuild
{
Version = "2018.712.0",
@ -155,6 +158,8 @@ namespace osu.Game.Tests.Visual.Online
AddUntilStep(@"wait for streams", () => changelog.Streams?.Count > 0);
AddAssert(@"correct build displayed", () => changelog.Current.Value.Version == "2018.712.0");
AddAssert(@"correct stream selected", () => changelog.Header.Streams.Current.Value.Id == 5);
AddUntilStep(@"wait for content load", () => changelog.ChildrenOfType<ChangelogSupporterPromo>().Any());
AddAssert(@"supporter promo showed", () => changelog.ChildrenOfType<ChangelogSupporterPromo>().First().Alpha == (isSupporter ? 0 : 1));
}
[Test]

View File

@ -0,0 +1,35 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Overlays;
using osu.Game.Overlays.Changelog;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneChangelogSupporterPromo : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
public TestSceneChangelogSupporterPromo()
{
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4,
},
new ChangelogSupporterPromo(),
}
};
}
}
}

View File

@ -3,84 +3,52 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics;
using osu.Game.Overlays.Comments;
using osu.Game.Overlays;
using osu.Framework.Allocation;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Users;
using osu.Game.Graphics.UserInterface;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics;
using osuTK;
using JetBrains.Annotations;
using NUnit.Framework;
using osu.Framework.Testing;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneCommentsPage : OsuTestScene
public class TestSceneOfflineCommentsContainer : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
private readonly BindableBool showDeleted = new BindableBool();
private readonly Container content;
private TestCommentsContainer comments;
private TestCommentsPage commentsPage;
public TestSceneCommentsPage()
[SetUp]
public void SetUp() => Schedule(() =>
{
Add(new FillFlowContainer
Clear();
Add(new BasicScrollContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 10),
Children = new Drawable[]
{
new Container
{
AutoSizeAxes = Axes.Y,
Width = 200,
Child = new OsuCheckbox
{
Current = showDeleted,
LabelText = @"Show Deleted"
}
},
content = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
}
}
RelativeSizeAxes = Axes.Both,
Child = comments = new TestCommentsContainer()
});
}
});
[Test]
public void TestAppendDuplicatedComment()
{
AddStep("Create page", () => createPage(getCommentBundle()));
AddAssert("Dictionary length is 10", () => commentsPage?.DictionaryLength == 10);
AddStep("Append existing comment", () => commentsPage?.AppendComments(getCommentSubBundle()));
AddAssert("Dictionary length is 10", () => commentsPage?.DictionaryLength == 10);
AddStep("Add comment bundle", () => comments.ShowComments(getCommentBundle()));
AddUntilStep("Dictionary length is 10", () => comments.DictionaryLength == 10);
AddStep("Append existing comment", () => comments.AppendComments(getCommentSubBundle()));
AddAssert("Dictionary length is 10", () => comments.DictionaryLength == 10);
}
[Test]
public void TestEmptyBundle()
public void TestLocalCommentBundle()
{
AddStep("Create page", () => createPage(getEmptyCommentBundle()));
AddAssert("Dictionary length is 0", () => commentsPage?.DictionaryLength == 0);
}
private void createPage(CommentBundle commentBundle)
{
commentsPage = null;
content.Clear();
content.Add(commentsPage = new TestCommentsPage(commentBundle)
{
ShowDeleted = { BindTarget = showDeleted }
});
AddStep("Add comment bundle", () => comments.ShowComments(getCommentBundle()));
AddStep("Add empty comment bundle", () => comments.ShowComments(getEmptyCommentBundle()));
}
private CommentBundle getEmptyCommentBundle() => new CommentBundle
@ -193,6 +161,7 @@ namespace osu.Game.Tests.Visual.Online
Username = "Good_Admin"
}
},
Total = 10
};
private CommentBundle getCommentSubBundle() => new CommentBundle
@ -211,16 +180,18 @@ namespace osu.Game.Tests.Visual.Online
IncludedComments = new List<Comment>(),
};
private class TestCommentsPage : CommentsPage
private class TestCommentsContainer : CommentsContainer
{
public TestCommentsPage(CommentBundle commentBundle)
: base(commentBundle)
{
}
public new void AppendComments([NotNull] CommentBundle bundle) => base.AppendComments(bundle);
public int DictionaryLength => CommentDictionary.Count;
public void ShowComments(CommentBundle bundle)
{
this.ChildrenOfType<TotalCommentsCounter>().Single().Current.Value = 0;
ClearComments();
OnSuccess(bundle);
}
}
}
}

View File

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

View File

@ -16,7 +16,8 @@ using osuTK;
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;
@ -29,7 +30,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
private readonly Drawable score1Bar;
private readonly Drawable score2Bar;
public MatchScoreDisplay()
public TournamentMatchScoreDisplay()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;

View File

@ -86,7 +86,7 @@ namespace osu.Game.Tournament.Screens.Gameplay
},
}
},
scoreDisplay = new MatchScoreDisplay
scoreDisplay = new TournamentMatchScoreDisplay
{
Y = -147,
Anchor = Anchor.BottomCentre,
@ -148,7 +148,7 @@ namespace osu.Game.Tournament.Screens.Gameplay
}
private ScheduledDelegate scheduledOperation;
private MatchScoreDisplay scoreDisplay;
private TournamentMatchScoreDisplay scoreDisplay;
private TourneyState lastState;
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 readonly Color4 COLOUR_RED = Color4Extensions.FromHex("#AA1414");
public static readonly Color4 COLOUR_BLUE = Color4Extensions.FromHex("#1462AA");
public static readonly Color4 COLOUR_RED = new OsuColour().TeamColourRed;
public static readonly Color4 COLOUR_BLUE = new OsuColour().TeamColourBlue;
public static readonly Color4 ELEMENT_BACKGROUND_COLOUR = Color4Extensions.FromHex("#fff");
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);
}
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
public readonly Color4 PurpleLighter = Color4Extensions.FromHex(@"eeeeff");
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>
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>
/// As the host, update the settings of the currently joined room.
/// </summary>

View File

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

View File

@ -91,6 +91,14 @@ namespace osu.Game.Online.Multiplayer
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)
{
if (!IsConnected.Value)

View File

@ -8,7 +8,7 @@ namespace osu.Game.Online.Rooms.RoomStatuses
{
public class RoomStatusEnded : RoomStatus
{
public override string Message => @"Ended";
public override string Message => "Ended";
public override Color4 GetAppropriateColour(OsuColour colours) => colours.YellowDarker;
}
}

View File

@ -8,7 +8,7 @@ namespace osu.Game.Online.Rooms.RoomStatuses
{
public class RoomStatusOpen : RoomStatus
{
public override string Message => @"Welcoming Players";
public override string Message => "Open";
public override Color4 GetAppropriateColour(OsuColour colours) => colours.GreenLight;
}
}

View File

@ -8,7 +8,7 @@ namespace osu.Game.Online.Rooms.RoomStatuses
{
public class RoomStatusPlaying : RoomStatus
{
public override string Message => @"Now Playing";
public override string Message => "Playing";
public override Color4 GetAppropriateColour(OsuColour colours) => colours.Purple;
}
}

View File

@ -71,6 +71,17 @@ namespace osu.Game.Overlays.Changelog
Colour = colourProvider.Background6,
Margin = new MarginPadding { Top = 30 },
},
new ChangelogSupporterPromo
{
Alpha = api.LocalUser.Value.IsSupporter ? 0 : 1,
},
new Box
{
RelativeSizeAxes = Axes.X,
Height = 2,
Colour = colourProvider.Background6,
Alpha = api.LocalUser.Value.IsSupporter ? 0 : 1,
},
comments = new CommentsContainer()
};

View File

@ -0,0 +1,187 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Chat;
using osu.Game.Resources.Localisation.Web;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Changelog
{
public class ChangelogSupporterPromo : CompositeDrawable
{
private const float image_container_width = 164;
private readonly FillFlowContainer textContainer;
private readonly Container imageContainer;
public ChangelogSupporterPromo()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Padding = new MarginPadding
{
Vertical = 20,
Horizontal = 50,
};
InternalChildren = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Masking = true,
CornerRadius = 6,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Colour = Color4.Black.Opacity(0.25f),
Offset = new Vector2(0, 1),
Radius = 3,
},
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black.Opacity(0.3f),
},
new Container
{
RelativeSizeAxes = Axes.X,
Height = 200,
Padding = new MarginPadding { Horizontal = 75 },
Children = new Drawable[]
{
textContainer = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Padding = new MarginPadding { Right = 50 + image_container_width },
},
imageContainer = new Container
{
RelativeSizeAxes = Axes.Y,
Width = image_container_width,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
}
}
},
}
},
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colour, TextureStore textures)
{
SupporterPromoLinkFlowContainer supportLinkText;
textContainer.Children = new Drawable[]
{
new OsuSpriteText
{
Text = ChangelogStrings.SupportHeading,
Font = OsuFont.GetFont(size: 20, weight: FontWeight.Light),
Margin = new MarginPadding { Bottom = 20 },
},
supportLinkText = new SupporterPromoLinkFlowContainer(t =>
{
t.Font = t.Font.With(size: 14);
t.Colour = colour.PinkLighter;
})
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
new OsuTextFlowContainer(t =>
{
t.Font = t.Font.With(size: 12);
t.Colour = colour.PinkLighter;
})
{
Text = ChangelogStrings.SupportText2.ToString(),
Margin = new MarginPadding { Top = 10 },
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
}
};
supportLinkText.AddText("Support further development of osu! and ");
supportLinkText.AddLink("become and osu!supporter", "https://osu.ppy.sh/home/support", t => t.Font = t.Font.With(weight: FontWeight.Bold));
supportLinkText.AddText(" today!");
imageContainer.Children = new Drawable[]
{
new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fill,
Texture = textures.Get(@"Online/supporter-pippi"),
},
new Sprite
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Width = 75,
Height = 75,
Margin = new MarginPadding { Top = 70 },
Texture = textures.Get(@"Online/supporter-heart"),
},
};
}
private class SupporterPromoLinkFlowContainer : LinkFlowContainer
{
public SupporterPromoLinkFlowContainer(Action<SpriteText> defaultCreationParameters)
: base(defaultCreationParameters)
{
}
public new void AddLink(string text, string url, Action<SpriteText> creationParameters) =>
AddInternal(new SupporterPromoLinkCompiler(AddText(text, creationParameters)) { Url = url });
private class SupporterPromoLinkCompiler : DrawableLinkCompiler
{
[Resolved(CanBeNull = true)]
private OsuGame game { get; set; }
public string Url;
public SupporterPromoLinkCompiler(IEnumerable<Drawable> parts)
: base(parts)
{
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(OsuColour colour)
{
TooltipText = Url;
Action = () => game?.HandleLink(Url);
IdleColour = colour.PinkDark;
HoverColour = Color4.White;
}
}
}
}
}

View File

@ -14,6 +14,9 @@ using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Threading;
using osu.Game.Users;
using System.Collections.Generic;
using JetBrains.Annotations;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Overlays.Comments
{
@ -147,7 +150,7 @@ namespace osu.Game.Overlays.Comments
private void refetchComments()
{
clearComments();
ClearComments();
getComments();
}
@ -160,50 +163,125 @@ namespace osu.Game.Overlays.Comments
loadCancellation?.Cancel();
scheduledCommentsLoad?.Cancel();
request = new GetCommentsRequest(id.Value, type.Value, Sort.Value, currentPage++, 0);
request.Success += res => scheduledCommentsLoad = Schedule(() => onSuccess(res));
request.Success += res => scheduledCommentsLoad = Schedule(() => OnSuccess(res));
api.PerformAsync(request);
}
private void clearComments()
protected void ClearComments()
{
currentPage = 1;
deletedCommentsCounter.Count.Value = 0;
moreButton.Show();
moreButton.IsLoading = true;
content.Clear();
CommentDictionary.Clear();
}
private void onSuccess(CommentBundle response)
protected readonly Dictionary<long, DrawableComment> CommentDictionary = new Dictionary<long, DrawableComment>();
protected void OnSuccess(CommentBundle response)
{
loadCancellation = new CancellationTokenSource();
commentCounter.Current.Value = response.Total;
LoadComponentAsync(new CommentsPage(response)
if (!response.Comments.Any())
{
ShowDeleted = { BindTarget = ShowDeleted },
Sort = { BindTarget = Sort },
Type = { BindTarget = type },
CommentableId = { BindTarget = id }
}, loaded =>
content.Add(new NoCommentsPlaceholder());
moreButton.Hide();
return;
}
AppendComments(response);
}
/// <summary>
/// Appends retrieved comments to the subtree rooted of comments in this page.
/// </summary>
/// <param name="bundle">The bundle of comments to add.</param>
protected void AppendComments([NotNull] CommentBundle bundle)
{
var topLevelComments = new List<DrawableComment>();
var orphaned = new List<Comment>();
foreach (var comment in bundle.Comments.Concat(bundle.IncludedComments))
{
content.Add(loaded);
// Exclude possible duplicated comments.
if (CommentDictionary.ContainsKey(comment.Id))
continue;
deletedCommentsCounter.Count.Value += response.Comments.Count(c => c.IsDeleted && c.IsTopLevel);
addNewComment(comment);
}
if (response.HasMore)
// Comments whose parents were seen later than themselves can now be added.
foreach (var o in orphaned)
addNewComment(o);
if (topLevelComments.Any())
{
LoadComponentsAsync(topLevelComments, loaded =>
{
int loadedTopLevelComments = 0;
content.Children.OfType<FillFlowContainer>().ForEach(p => loadedTopLevelComments += p.Children.OfType<DrawableComment>().Count());
content.AddRange(loaded);
moreButton.Current.Value = response.TopLevelCount - loadedTopLevelComments;
moreButton.IsLoading = false;
deletedCommentsCounter.Count.Value += topLevelComments.Select(d => d.Comment).Count(c => c.IsDeleted && c.IsTopLevel);
if (bundle.HasMore)
{
int loadedTopLevelComments = 0;
content.Children.OfType<DrawableComment>().ForEach(p => loadedTopLevelComments++);
moreButton.Current.Value = bundle.TopLevelCount - loadedTopLevelComments;
moreButton.IsLoading = false;
}
else
{
moreButton.Hide();
}
}, (loadCancellation = new CancellationTokenSource()).Token);
}
void addNewComment(Comment comment)
{
var drawableComment = getDrawableComment(comment);
if (comment.ParentId == null)
{
// Comments that have no parent are added as top-level comments to the flow.
topLevelComments.Add(drawableComment);
}
else if (CommentDictionary.TryGetValue(comment.ParentId.Value, out var parentDrawable))
{
// The comment's parent has already been seen, so the parent<-> child links can be added.
comment.ParentComment = parentDrawable.Comment;
parentDrawable.Replies.Add(drawableComment);
}
else
{
moreButton.Hide();
// The comment's parent has not been seen yet, so keep it orphaned for the time being. This can occur if the comments arrive out of order.
// Since this comment has now been seen, any further children can be added to it without being orphaned themselves.
orphaned.Add(comment);
}
}
}
commentCounter.Current.Value = response.Total;
}, loadCancellation.Token);
private DrawableComment getDrawableComment(Comment comment)
{
if (CommentDictionary.TryGetValue(comment.Id, out var existing))
return existing;
return CommentDictionary[comment.Id] = new DrawableComment(comment)
{
ShowDeleted = { BindTarget = ShowDeleted },
Sort = { BindTarget = Sort },
RepliesRequested = onCommentRepliesRequested
};
}
private void onCommentRepliesRequested(DrawableComment drawableComment, int page)
{
var req = new GetCommentsRequest(id.Value, type.Value, Sort.Value, page, drawableComment.Comment.Id);
req.Success += response => Schedule(() => AppendComments(response));
api.PerformAsync(req);
}
protected override void Dispose(bool isDisposing)
@ -212,5 +290,30 @@ namespace osu.Game.Overlays.Comments
loadCancellation?.Cancel();
base.Dispose(isDisposing);
}
private class NoCommentsPlaceholder : CompositeDrawable
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Height = 80;
RelativeSizeAxes = Axes.X;
AddRangeInternal(new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Left = 50 },
Text = @"No comments yet."
}
});
}
}
}
}

View File

@ -1,161 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics;
using osu.Framework.Bindables;
using osu.Game.Online.API.Requests.Responses;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Sprites;
using System.Linq;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API;
using System.Collections.Generic;
using JetBrains.Annotations;
namespace osu.Game.Overlays.Comments
{
public class CommentsPage : CompositeDrawable
{
public readonly BindableBool ShowDeleted = new BindableBool();
public readonly Bindable<CommentsSortCriteria> Sort = new Bindable<CommentsSortCriteria>();
public readonly Bindable<CommentableType> Type = new Bindable<CommentableType>();
public readonly BindableLong CommentableId = new BindableLong();
[Resolved]
private IAPIProvider api { get; set; }
private readonly CommentBundle commentBundle;
private FillFlowContainer flow;
public CommentsPage(CommentBundle commentBundle)
{
this.commentBundle = commentBundle;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
AddRangeInternal(new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5
},
flow = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
}
});
if (!commentBundle.Comments.Any())
{
flow.Add(new NoCommentsPlaceholder());
return;
}
AppendComments(commentBundle);
}
private DrawableComment getDrawableComment(Comment comment)
{
if (CommentDictionary.TryGetValue(comment.Id, out var existing))
return existing;
return CommentDictionary[comment.Id] = new DrawableComment(comment)
{
ShowDeleted = { BindTarget = ShowDeleted },
Sort = { BindTarget = Sort },
RepliesRequested = onCommentRepliesRequested
};
}
private void onCommentRepliesRequested(DrawableComment drawableComment, int page)
{
var request = new GetCommentsRequest(CommentableId.Value, Type.Value, Sort.Value, page, drawableComment.Comment.Id);
request.Success += response => Schedule(() => AppendComments(response));
api.PerformAsync(request);
}
protected readonly Dictionary<long, DrawableComment> CommentDictionary = new Dictionary<long, DrawableComment>();
/// <summary>
/// Appends retrieved comments to the subtree rooted of comments in this page.
/// </summary>
/// <param name="bundle">The bundle of comments to add.</param>
protected void AppendComments([NotNull] CommentBundle bundle)
{
var orphaned = new List<Comment>();
foreach (var comment in bundle.Comments.Concat(bundle.IncludedComments))
{
// Exclude possible duplicated comments.
if (CommentDictionary.ContainsKey(comment.Id))
continue;
addNewComment(comment);
}
// Comments whose parents were seen later than themselves can now be added.
foreach (var o in orphaned)
addNewComment(o);
void addNewComment(Comment comment)
{
var drawableComment = getDrawableComment(comment);
if (comment.ParentId == null)
{
// Comments that have no parent are added as top-level comments to the flow.
flow.Add(drawableComment);
}
else if (CommentDictionary.TryGetValue(comment.ParentId.Value, out var parentDrawable))
{
// The comment's parent has already been seen, so the parent<-> child links can be added.
comment.ParentComment = parentDrawable.Comment;
parentDrawable.Replies.Add(drawableComment);
}
else
{
// The comment's parent has not been seen yet, so keep it orphaned for the time being. This can occur if the comments arrive out of order.
// Since this comment has now been seen, any further children can be added to it without being orphaned themselves.
orphaned.Add(comment);
}
}
}
private class NoCommentsPlaceholder : CompositeDrawable
{
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Height = 80;
RelativeSizeAxes = Axes.X;
AddRangeInternal(new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Left = 50 },
Text = @"No comments yet."
}
});
}
}
}
}

View File

@ -18,6 +18,7 @@ namespace osu.Game.Overlays
public static OverlayColourProvider Green { get; } = new OverlayColourProvider(OverlayColourScheme.Green);
public static OverlayColourProvider Purple { get; } = new OverlayColourProvider(OverlayColourScheme.Purple);
public static OverlayColourProvider Blue { get; } = new OverlayColourProvider(OverlayColourScheme.Blue);
public static OverlayColourProvider Plum { get; } = new OverlayColourProvider(OverlayColourScheme.Plum);
public OverlayColourProvider(OverlayColourScheme colourScheme)
{
@ -80,6 +81,9 @@ namespace osu.Game.Overlays
case OverlayColourScheme.Blue:
return 200 / 360f;
case OverlayColourScheme.Plum:
return 320 / 360f;
}
}
}
@ -92,6 +96,7 @@ namespace osu.Game.Overlays
Lime,
Green,
Purple,
Blue
Blue,
Plum,
}
}

View File

@ -1,117 +0,0 @@
// 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.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
namespace osu.Game.Screens.OnlinePlay.Components
{
public class RoomStatusInfo : OnlinePlayComposite
{
public RoomStatusInfo()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
StatusPart statusPart;
EndDatePart endDatePart;
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
statusPart = new StatusPart
{
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 14)
},
endDatePart = new EndDatePart { Font = OsuFont.GetFont(size: 14) }
}
};
statusPart.EndDate.BindTo(EndDate);
statusPart.Status.BindTo(Status);
statusPart.Availability.BindTo(Availability);
endDatePart.EndDate.BindTo(EndDate);
}
private class EndDatePart : DrawableDate
{
public readonly IBindable<DateTimeOffset?> EndDate = new Bindable<DateTimeOffset?>();
public EndDatePart()
: base(DateTimeOffset.UtcNow)
{
EndDate.BindValueChanged(date =>
{
// If null, set a very large future date to prevent unnecessary schedules.
Date = date.NewValue ?? DateTimeOffset.Now.AddYears(1);
}, true);
}
protected override string Format()
{
if (EndDate.Value == null)
return string.Empty;
var diffToNow = Date.Subtract(DateTimeOffset.Now);
if (diffToNow.TotalSeconds < -5)
return $"Closed {base.Format()}";
if (diffToNow.TotalSeconds < 0)
return "Closed";
if (diffToNow.TotalSeconds < 5)
return "Closing soon";
return $"Closing {base.Format()}";
}
}
private class StatusPart : EndDatePart
{
public readonly IBindable<RoomStatus> Status = new Bindable<RoomStatus>();
public readonly IBindable<RoomAvailability> Availability = new Bindable<RoomAvailability>();
[Resolved]
private OsuColour colours { get; set; }
public StatusPart()
{
EndDate.BindValueChanged(_ => Format());
Status.BindValueChanged(_ => Format());
Availability.BindValueChanged(_ => Format());
}
protected override void LoadComplete()
{
base.LoadComplete();
Text = Format();
}
protected override string Format()
{
if (!IsLoaded)
return string.Empty;
RoomStatus status = Date < DateTimeOffset.Now ? new RoomStatusEnded() : Status.Value ?? new RoomStatusOpen();
this.FadeColour(status.GetAppropriateColour(colours), 100);
return $"{Availability.Value.GetDescription()}, {status.Message}";
}
}
}
}

View File

@ -1,12 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Screens.Ranking.Expanded;
@ -85,6 +87,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
minDisplay.Current.Value = minDifficulty;
maxDisplay.Current.Value = maxDifficulty;
maxDisplay.Alpha = Precision.AlmostEquals(Math.Round(minDifficulty.Stars, 2), Math.Round(maxDifficulty.Stars, 2)) ? 0 : 1;
minBackground.Colour = colours.ForStarDifficulty(minDifficulty.Stars);
maxBackground.Colour = colours.ForStarDifficulty(maxDifficulty.Stars);

View File

@ -11,6 +11,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects;
@ -20,7 +21,6 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@ -28,6 +28,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Input.Bindings;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.Components;
using osuTK;
using osuTK.Graphics;
@ -37,21 +38,18 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
public class DrawableRoom : OsuClickableContainer, IStateful<SelectionState>, IFilterable, IHasContextMenu, IHasPopover, IKeyBindingHandler<GlobalAction>
{
public const float SELECTION_BORDER_WIDTH = 4;
private const float corner_radius = 5;
private const float corner_radius = 10;
private const float transition_duration = 60;
private const float content_padding = 10;
private const float height = 110;
private const float side_strip_width = 5;
private const float cover_width = 145;
private const float height = 100;
public event Action<SelectionState> StateChanged;
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds();
private readonly Box selectionBox;
private Drawable selectionBox;
[Resolved(canBeNull: true)]
private OnlinePlayScreen parentScreen { get; set; }
private LoungeSubScreen loungeScreen { get; set; }
[Resolved]
private BeatmapManager beatmaps { get; set; }
@ -74,14 +72,18 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
get => state;
set
{
if (value == state) return;
if (value == state)
return;
state = value;
if (state == SelectionState.Selected)
selectionBox.FadeIn(transition_duration);
else
selectionBox.FadeOut(transition_duration);
if (selectionBox != null)
{
if (state == SelectionState.Selected)
selectionBox.FadeIn(transition_duration);
else
selectionBox.FadeOut(transition_duration);
}
StateChanged?.Invoke(State);
}
@ -108,6 +110,25 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
}
}
private int numberOfAvatars = 7;
public int NumberOfAvatars
{
get => numberOfAvatars;
set
{
numberOfAvatars = value;
if (recentParticipantsList != null)
recentParticipantsList.NumberOfCircles = value;
}
}
private readonly Bindable<RoomCategory> roomCategory = new Bindable<RoomCategory>();
private RecentParticipantsList recentParticipantsList;
private RoomSpecialCategoryPill specialCategoryPill;
public bool FilteringActive { get; set; }
private PasswordProtectedIcon passwordIcon;
@ -119,114 +140,208 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
Room = room;
RelativeSizeAxes = Axes.X;
Height = height + SELECTION_BORDER_WIDTH * 2;
CornerRadius = corner_radius + SELECTION_BORDER_WIDTH / 2;
Masking = true;
Height = height;
// create selectionBox here so State can be set before being loaded
selectionBox = new Box
Masking = true;
CornerRadius = corner_radius + SELECTION_BORDER_WIDTH / 2;
EdgeEffect = new EdgeEffectParameters
{
RelativeSizeAxes = Axes.Both,
Alpha = 0f,
Type = EdgeEffectType.Shadow,
Colour = Color4.Black.Opacity(40),
Radius = 5,
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours, AudioManager audio)
private void load(OverlayColourProvider colours, AudioManager audio)
{
float stripWidth = side_strip_width * (Room.Category.Value == RoomCategory.Spotlight ? 2 : 1);
Children = new Drawable[]
{
new StatusColouredContainer(transition_duration)
// This resolves internal 1px gaps due to applying the (parenting) corner radius and masking across multiple filling background sprites.
new BufferedContainer
{
RelativeSizeAxes = Axes.Both,
Child = selectionBox
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Background5,
},
new OnlinePlayBackgroundSprite
{
RelativeSizeAxes = Axes.Both
},
}
},
new Container
{
Name = @"Room content",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(SELECTION_BORDER_WIDTH),
// This negative padding resolves 1px gaps between this background and the background above.
Padding = new MarginPadding { Left = 20, Vertical = -0.5f },
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = corner_radius,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Colour = Color4.Black.Opacity(40),
Radius = 5,
},
Children = new Drawable[]
{
new Box
// This resolves internal 1px gaps due to applying the (parenting) corner radius and masking across multiple filling background sprites.
new BufferedContainer
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex(@"212121"),
},
new StatusColouredContainer(transition_duration)
{
RelativeSizeAxes = Axes.Y,
Width = stripWidth,
Child = new Box { RelativeSizeAxes = Axes.Both }
},
new Container
{
RelativeSizeAxes = Axes.Y,
Width = cover_width,
Masking = true,
Margin = new MarginPadding { Left = stripWidth },
Child = new OnlinePlayBackgroundSprite(BeatmapSetCoverType.List) { RelativeSizeAxes = Axes.Both }
Children = new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Relative, 0.2f)
},
Content = new[]
{
new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Background5,
},
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal(colours.Background5, colours.Background5.Opacity(0.3f))
},
}
}
},
},
},
new Container
{
Name = @"Left details",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Vertical = content_padding,
Left = stripWidth + cover_width + content_padding,
Right = content_padding,
Left = 20,
Vertical = 5
},
Children = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5f),
Children = new Drawable[]
{
new RoomName { Font = OsuFont.GetFont(size: 18) },
new ParticipantInfo(),
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Children = new Drawable[]
{
new RoomStatusPill
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
},
specialCategoryPill = new RoomSpecialCategoryPill
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
},
new EndDateInfo
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
},
}
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = 3 },
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new RoomNameText(),
new RoomHostText(),
}
}
},
},
new FillFlowContainer
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Children = new Drawable[]
{
new RoomStatusInfo(),
new BeatmapTitle { TextSize = 14 },
},
},
new ModeTypeInfo
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
},
new PlaylistCountPill
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
new StarRatingRangeDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Scale = new Vector2(0.8f)
}
}
}
}
},
new FillFlowContainer
{
Name = "Right content",
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Padding = new MarginPadding
{
Right = 10,
Vertical = 5
},
Children = new Drawable[]
{
recentParticipantsList = new RecentParticipantsList
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
NumberOfCircles = NumberOfAvatars
}
}
},
passwordIcon = new PasswordProtectedIcon { Alpha = 0 }
},
},
},
new StatusColouredContainer(transition_duration)
{
RelativeSizeAxes = Axes.Both,
Child = selectionBox = new Container
{
RelativeSizeAxes = Axes.Both,
Alpha = state == SelectionState.Selected ? 1 : 0,
Masking = true,
CornerRadius = corner_radius,
BorderThickness = SELECTION_BORDER_WIDTH,
BorderColour = Color4.White,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
}
}
},
};
sampleSelect = audio.Samples.Get($@"UI/{HoverSampleSet.Default.GetDescription()}-select");
@ -250,6 +365,15 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
else
Alpha = 0;
roomCategory.BindTo(Room.Category);
roomCategory.BindValueChanged(c =>
{
if (c.NewValue == RoomCategory.Spotlight)
specialCategoryPill.Show();
else
specialCategoryPill.Hide();
}, true);
hasPassword.BindTo(Room.HasPassword);
hasPassword.BindValueChanged(v => passwordIcon.Alpha = v.NewValue ? 1 : 0, true);
}
@ -260,7 +384,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
new OsuMenuItem("Create copy", MenuItemType.Standard, () =>
{
parentScreen?.OpenNewRoom(Room.DeepClone());
lounge?.Open(Room.DeepClone());
})
};
@ -307,11 +431,16 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
return base.OnClick(e);
}
private class RoomName : OsuSpriteText
private class RoomNameText : OsuSpriteText
{
[Resolved(typeof(Room), nameof(Online.Rooms.Room.Name))]
private Bindable<string> name { get; set; }
public RoomNameText()
{
Font = OsuFont.GetFont(size: 28);
}
[BackgroundDependencyLoader]
private void load()
{
@ -319,6 +448,41 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
}
}
private class RoomHostText : OnlinePlayComposite
{
private LinkFlowContainer hostText;
public RoomHostText()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = hostText = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 16))
{
AutoSizeAxes = Axes.Both
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Host.BindValueChanged(host =>
{
hostText.Clear();
if (host.NewValue != null)
{
hostText.AddText("hosted by ");
hostText.AddUserLink(host.NewValue);
}
}, true);
}
}
public class PasswordProtectedIcon : CompositeDrawable
{
[BackgroundDependencyLoader]
@ -366,7 +530,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
private OsuPasswordTextBox passwordTextbox;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
private void load()
{
Child = new FillFlowContainer
{

View File

@ -0,0 +1,65 @@
// 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.Game.Graphics;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public class EndDateInfo : OnlinePlayComposite
{
public EndDateInfo()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new EndDatePart
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 12),
EndDate = { BindTarget = EndDate }
};
}
private class EndDatePart : DrawableDate
{
public readonly IBindable<DateTimeOffset?> EndDate = new Bindable<DateTimeOffset?>();
public EndDatePart()
: base(DateTimeOffset.UtcNow)
{
EndDate.BindValueChanged(date =>
{
// If null, set a very large future date to prevent unnecessary schedules.
Date = date.NewValue ?? DateTimeOffset.Now.AddYears(1);
}, true);
}
protected override string Format()
{
if (EndDate.Value == null)
return string.Empty;
var diffToNow = Date.Subtract(DateTimeOffset.Now);
if (diffToNow.TotalSeconds < -5)
return $"Closed {base.Format()}";
if (diffToNow.TotalSeconds < 0)
return "Closed";
if (diffToNow.TotalSeconds < 5)
return "Closing soon";
return $"Closing {base.Format()}";
}
}
}
}

View File

@ -5,19 +5,18 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Threading;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osuTK.Graphics;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public abstract class FilterControl : CompositeDrawable
{
protected const float VERTICAL_PADDING = 10;
protected const float HORIZONTAL_PADDING = 80;
protected readonly FillFlowContainer Filters;
[Resolved(CanBeNull = true)]
private Bindable<FilterCriteria> filter { get; set; }
@ -25,60 +24,51 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; }
private readonly Box tabStrip;
private readonly SearchTextBox search;
private readonly PageTabControl<RoomStatusFilter> tabs;
private readonly Dropdown<RoomStatusFilter> statusDropdown;
protected FilterControl()
{
InternalChildren = new Drawable[]
RelativeSizeAxes = Axes.X;
Height = 70;
InternalChild = new FillFlowContainer
{
new Box
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(10),
Children = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 0.25f,
},
tabStrip = new Box
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Height = 1,
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
search = new FilterSearchTextBox
{
Top = VERTICAL_PADDING,
Horizontal = HORIZONTAL_PADDING
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.X,
Width = 0.6f,
},
Children = new Drawable[]
Filters = new FillFlowContainer
{
search = new FilterSearchTextBox
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10),
Child = statusDropdown = new SlimEnumDropdown<RoomStatusFilter>
{
RelativeSizeAxes = Axes.X,
},
tabs = new PageTabControl<RoomStatusFilter>
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
},
}
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.None,
Width = 160,
}
},
}
};
tabs.Current.Value = RoomStatusFilter.Open;
tabs.Current.TriggerChange();
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
filter ??= new Bindable<FilterCriteria>();
tabStrip.Colour = colours.Yellow;
}
protected override void LoadComplete()
@ -87,7 +77,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
search.Current.BindValueChanged(_ => updateFilterDebounced());
ruleset.BindValueChanged(_ => UpdateFilter());
tabs.Current.BindValueChanged(_ => UpdateFilter(), true);
statusDropdown.Current.BindValueChanged(_ => UpdateFilter(), true);
}
private ScheduledDelegate scheduledFilterUpdate;
@ -106,7 +96,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
var criteria = CreateCriteria();
criteria.SearchString = search.Current.Value;
criteria.Status = tabs.Current.Value;
criteria.Status = statusDropdown.Current.Value;
criteria.Ruleset = ruleset.Value;
filter.Value = criteria;

View File

@ -1,88 +0,0 @@
// 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 Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Users.Drawables;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public class ParticipantInfo : OnlinePlayComposite
{
public ParticipantInfo()
{
RelativeSizeAxes = Axes.X;
Height = 15f;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
OsuSpriteText summary;
Container flagContainer;
LinkFlowContainer hostText;
InternalChildren = new Drawable[]
{
new FillFlowContainer
{
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5f, 0f),
Children = new Drawable[]
{
flagContainer = new Container
{
Width = 22f,
RelativeSizeAxes = Axes.Y,
},
hostText = new LinkFlowContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both
}
},
},
new FillFlowContainer
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Colour = colours.Gray9,
Children = new[]
{
summary = new OsuSpriteText
{
Text = "0 participants",
}
},
},
};
Host.BindValueChanged(host =>
{
hostText.Clear();
flagContainer.Clear();
if (host.NewValue != null)
{
hostText.AddText("hosted by ");
hostText.AddUserLink(host.NewValue, s => s.Font = s.Font.With(Typeface.Torus, weight: FontWeight.Bold, italics: true));
flagContainer.Child = new UpdateableFlag(host.NewValue.Country) { RelativeSizeAxes = Axes.Both };
}
}, true);
ParticipantCount.BindValueChanged(count => summary.Text = "participant".ToQuantity(count.NewValue), true);
}
}
}

View File

@ -0,0 +1,81 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
/// <summary>
/// Displays contents in a "pill".
/// </summary>
public class PillContainer : Container
{
private const float padding = 8;
public readonly Drawable Background;
protected override Container<Drawable> Content => content;
private readonly Container content;
public PillContainer()
{
AutoSizeAxes = Axes.X;
Height = 16;
InternalChild = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Masking = true,
Children = new[]
{
Background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 0.5f
},
new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = padding },
Child = new GridContainer
{
AutoSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize, minSize: 80 - 2 * padding)
},
Content = new[]
{
new[]
{
new Container
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Padding = new MarginPadding { Bottom = 2 },
Child = content = new Container
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
}
}
}
}
}
}
};
}
}
}

View File

@ -0,0 +1,54 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Specialized;
using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
/// <summary>
/// A pill that displays the playlist item count.
/// </summary>
public class PlaylistCountPill : OnlinePlayComposite
{
private OsuTextFlowContainer count;
public PlaylistCountPill()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new PillContainer
{
Child = count = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12))
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Playlist.BindCollectionChanged(updateCount, true);
}
private void updateCount(object sender, NotifyCollectionChangedEventArgs e)
{
count.Clear();
count.AddText(Playlist.Count.ToString(), s => s.Font = s.Font.With(weight: FontWeight.Bold));
count.AddText(" ");
count.AddText("Beatmap".ToQuantity(Playlist.Count, ShowQuantityAs.None));
}
}
}

View File

@ -9,18 +9,16 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public class PlaylistsFilterControl : FilterControl
{
private readonly Dropdown<PlaylistsCategory> dropdown;
private readonly Dropdown<PlaylistsCategory> categoryDropdown;
public PlaylistsFilterControl()
{
AddInternal(dropdown = new SlimEnumDropdown<PlaylistsCategory>
Filters.Add(categoryDropdown = new SlimEnumDropdown<PlaylistsCategory>
{
Anchor = Anchor.BottomRight,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.None,
Width = 160,
X = -HORIZONTAL_PADDING,
Y = -30
});
}
@ -28,14 +26,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
base.LoadComplete();
dropdown.Current.BindValueChanged(_ => UpdateFilter());
categoryDropdown.Current.BindValueChanged(_ => UpdateFilter());
}
protected override FilterCriteria CreateCriteria()
{
var criteria = base.CreateCriteria();
switch (dropdown.Current.Value)
switch (categoryDropdown.Current.Value)
{
case PlaylistsCategory.Normal:
criteria.Category = "normal";

View File

@ -0,0 +1,80 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public class RankRangePill : MultiplayerRoomComposite
{
private OsuTextFlowContainer rankFlow;
public RankRangePill()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new PillContainer
{
Child = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(4),
Children = new Drawable[]
{
new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(8),
Icon = FontAwesome.Solid.User
},
rankFlow = new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12))
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
}
}
}
};
}
protected override void OnRoomUpdated()
{
base.OnRoomUpdated();
rankFlow.Clear();
if (Room == null || Room.Users.All(u => u.User == null))
{
rankFlow.AddText("-");
return;
}
int minRank = Room.Users.Select(u => u.User?.Statistics.GlobalRank ?? 0).DefaultIfEmpty(0).Min();
int maxRank = Room.Users.Select(u => u.User?.Statistics.GlobalRank ?? 0).DefaultIfEmpty(0).Max();
rankFlow.AddText("#");
rankFlow.AddText(minRank.ToString("#,0"), s => s.Font = s.Font.With(weight: FontWeight.Bold));
rankFlow.AddText(" - ");
rankFlow.AddText("#");
rankFlow.AddText(maxRank.ToString("#,0"), s => s.Font = s.Font.With(weight: FontWeight.Bold));
}
}
}

View File

@ -0,0 +1,278 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Users;
using osu.Game.Users.Drawables;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public class RecentParticipantsList : OnlinePlayComposite
{
private const float avatar_size = 36;
private FillFlowContainer<CircularAvatar> avatarFlow;
private HiddenUserCount hiddenUsers;
private OsuSpriteText totalCount;
public RecentParticipantsList()
{
AutoSizeAxes = Axes.X;
Height = 60;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colours)
{
InternalChildren = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 10,
Shear = new Vector2(0.2f, 0),
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Background4,
}
},
new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(4),
Padding = new MarginPadding { Right = 16 },
Children = new Drawable[]
{
new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(16),
Margin = new MarginPadding { Left = 8 },
Icon = FontAwesome.Solid.User,
},
totalCount = new OsuSpriteText
{
Font = OsuFont.Default.With(weight: FontWeight.Bold),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
avatarFlow = new FillFlowContainer<CircularAvatar>
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(4),
Margin = new MarginPadding { Left = 4 },
},
hiddenUsers = new HiddenUserCount
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
}
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
RecentParticipants.BindCollectionChanged(onParticipantsChanged, true);
ParticipantCount.BindValueChanged(_ =>
{
updateHiddenUsers();
totalCount.Text = ParticipantCount.Value.ToString();
}, true);
}
private int numberOfCircles = 4;
/// <summary>
/// The maximum number of circles visible (including the "hidden count" circle in the overflow case).
/// </summary>
public int NumberOfCircles
{
get => numberOfCircles;
set
{
numberOfCircles = value;
if (LoadState < LoadState.Loaded)
return;
// Reinitialising the list looks janky, but this is unlikely to be used in a setting where it's visible.
clearUsers();
foreach (var u in RecentParticipants)
addUser(u);
updateHiddenUsers();
}
}
private void onParticipantsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (var added in e.NewItems.OfType<User>())
addUser(added);
break;
case NotifyCollectionChangedAction.Remove:
foreach (var removed in e.OldItems.OfType<User>())
removeUser(removed);
break;
case NotifyCollectionChangedAction.Reset:
clearUsers();
break;
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Move:
// Easiest is to just reinitialise the whole list. These are unlikely to ever be use cases.
clearUsers();
foreach (var u in RecentParticipants)
addUser(u);
break;
}
updateHiddenUsers();
}
private int displayedCircles => avatarFlow.Count + (hiddenUsers.Count > 0 ? 1 : 0);
private void addUser(User user)
{
if (displayedCircles < NumberOfCircles)
avatarFlow.Add(new CircularAvatar { User = user });
}
private void removeUser(User user)
{
avatarFlow.RemoveAll(a => a.User == user);
}
private void clearUsers()
{
avatarFlow.Clear();
updateHiddenUsers();
}
private void updateHiddenUsers()
{
int hiddenCount = 0;
if (RecentParticipants.Count > NumberOfCircles)
hiddenCount = ParticipantCount.Value - NumberOfCircles + 1;
hiddenUsers.Count = hiddenCount;
if (displayedCircles > NumberOfCircles)
avatarFlow.Remove(avatarFlow.Last());
else if (displayedCircles < NumberOfCircles)
{
var nextUser = RecentParticipants.FirstOrDefault(u => avatarFlow.All(a => a.User != u));
if (nextUser != null) addUser(nextUser);
}
}
private class CircularAvatar : CompositeDrawable
{
public User User
{
get => avatar.User;
set => avatar.User = value;
}
private readonly UpdateableAvatar avatar = new UpdateableAvatar(showUsernameTooltip: true) { RelativeSizeAxes = Axes.Both };
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colours)
{
Size = new Vector2(avatar_size);
InternalChild = new CircularContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new Drawable[]
{
new Box
{
Colour = colours.Background5,
RelativeSizeAxes = Axes.Both,
},
avatar
}
};
}
}
public class HiddenUserCount : CompositeDrawable
{
public int Count
{
get => count;
set
{
count = value;
countText.Text = $"+{count}";
if (count > 0)
Show();
else
Hide();
}
}
private int count;
private readonly SpriteText countText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Default.With(weight: FontWeight.Bold),
};
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colours)
{
Size = new Vector2(avatar_size);
Alpha = 0;
InternalChild = new CircularContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Background5,
},
countText
}
};
}
}
}
}

View File

@ -1,86 +0,0 @@
// 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 osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Screens.OnlinePlay.Components;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public class RoomInfo : OnlinePlayComposite
{
private readonly List<Drawable> statusElements = new List<Drawable>();
private readonly OsuTextFlowContainer roomName;
public RoomInfo()
{
AutoSizeAxes = Axes.Y;
RoomLocalUserInfo localUserInfo;
RoomStatusInfo statusInfo;
ModeTypeInfo typeInfo;
ParticipantInfo participantInfo;
InternalChild = new FillFlowContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X,
Spacing = new Vector2(0, 10),
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
roomName = new OsuTextFlowContainer(t => t.Font = OsuFont.GetFont(size: 30))
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
participantInfo = new ParticipantInfo(),
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
statusInfo = new RoomStatusInfo(),
typeInfo = new ModeTypeInfo
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight
}
}
},
localUserInfo = new RoomLocalUserInfo(),
}
};
statusElements.AddRange(new Drawable[]
{
statusInfo, typeInfo, participantInfo, localUserInfo
});
}
protected override void LoadComplete()
{
base.LoadComplete();
if (RoomID.Value == null)
statusElements.ForEach(e => e.FadeOut());
RoomID.BindValueChanged(id =>
{
if (id.NewValue == null)
statusElements.ForEach(e => e.FadeOut(100));
else
statusElements.ForEach(e => e.FadeIn(100));
}, true);
RoomName.BindValueChanged(name =>
{
roomName.Text = name.NewValue ?? "No room selected";
}, true);
}
}
}

View File

@ -1,91 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Screens.OnlinePlay.Components;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public class RoomInspector : OnlinePlayComposite
{
private const float transition_duration = 100;
private readonly MarginPadding contentPadding = new MarginPadding { Horizontal = 20, Vertical = 10 };
[Resolved]
private BeatmapManager beatmaps { get; set; }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
OverlinedHeader participantsHeader;
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 0.25f
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = 30 },
Child = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new RoomInfo
{
RelativeSizeAxes = Axes.X,
Margin = new MarginPadding { Vertical = 60 },
},
participantsHeader = new OverlinedHeader("Recent Participants"),
new ParticipantsDisplay(Direction.Vertical)
{
RelativeSizeAxes = Axes.X,
Height = ParticipantsList.TILE_SIZE * 3,
Details = { BindTarget = participantsHeader.Details }
}
}
}
},
new Drawable[] { new OverlinedPlaylistHeader(), },
new Drawable[]
{
new DrawableRoomPlaylist(false, false)
{
RelativeSizeAxes = Axes.Both,
Items = { BindTarget = Playlist }
},
},
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
}
}
}
};
}
}
}

View File

@ -0,0 +1,49 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public class RoomSpecialCategoryPill : OnlinePlayComposite
{
private SpriteText text;
public RoomSpecialCategoryPill()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
InternalChild = new PillContainer
{
Background =
{
Colour = colours.Pink,
Alpha = 1
},
Child = text = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 12),
Colour = Color4.Black
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Category.BindValueChanged(c => text.Text = c.NewValue.ToString(), true);
}
}
}

View File

@ -0,0 +1,74 @@
// 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.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
/// <summary>
/// A pill that displays the room's current status.
/// </summary>
public class RoomStatusPill : OnlinePlayComposite
{
[Resolved]
private OsuColour colours { get; set; }
private PillContainer pill;
private SpriteText statusText;
public RoomStatusPill()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = pill = new PillContainer
{
Child = statusText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 12),
Colour = Color4.Black
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
EndDate.BindValueChanged(_ => updateDisplay());
Status.BindValueChanged(_ => updateDisplay(), true);
FinishTransforms(true);
}
private void updateDisplay()
{
RoomStatus status = getDisplayStatus();
pill.Background.Alpha = 1;
pill.Background.FadeColour(status.GetAppropriateColour(colours), 100);
statusText.Text = status.Message;
}
private RoomStatus getDisplayStatus()
{
if (EndDate.Value < DateTimeOffset.Now)
return new RoomStatusEnded();
return Status.Value;
}
}
}

View File

@ -50,6 +50,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
// account for the fact we are in a scroll container and want a bit of spacing from the scroll bar.
Padding = new MarginPadding { Right = 5 };
InternalChild = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.X,
@ -59,7 +62,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(2),
Spacing = new Vector2(10),
}
};
}

View File

@ -9,6 +9,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Screens;
using osu.Game.Graphics.Containers;
@ -18,6 +19,8 @@ using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Users;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Lounge
{
@ -28,11 +31,17 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby();
protected Container<OsuButton> Buttons { get; } = new Container<OsuButton>
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
AutoSizeAxes = Axes.Both
};
private readonly IBindable<bool> initialRoomsReceived = new Bindable<bool>();
private readonly IBindable<bool> operationInProgress = new Bindable<bool>();
private FilterControl filter;
private Container content;
private LoadingLayer loadingLayer;
[Resolved]
@ -56,41 +65,71 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
InternalChildren = new Drawable[]
{
content = new Container
new Box
{
RelativeSizeAxes = Axes.X,
Height = 100,
Colour = Color4.Black,
Alpha = 0.5f,
},
new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
Padding = new MarginPadding
{
new Container
Top = 20,
Left = WaveOverlayContainer.WIDTH_PADDING,
Right = WaveOverlayContainer.WIDTH_PADDING,
},
Child = new GridContainer
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
RelativeSizeAxes = Axes.Both,
Width = 0.55f,
Children = new Drawable[]
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, 20)
},
Content = new[]
{
new Drawable[]
{
scrollContainer = new OsuScrollContainer
new Container
{
RelativeSizeAxes = Axes.X,
Height = 70,
Depth = -1,
Children = new Drawable[]
{
filter = CreateFilterControl(),
Buttons.WithChild(CreateNewRoomButton().With(d =>
{
d.Size = new Vector2(150, 25);
d.Action = () => Open();
}))
}
}
},
null,
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
ScrollbarOverlapsContent = false,
Padding = new MarginPadding(10),
Child = roomsContainer = new RoomsContainer()
Children = new Drawable[]
{
scrollContainer = new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarOverlapsContent = false,
Child = roomsContainer = new RoomsContainer()
},
loadingLayer = new LoadingLayer(true),
}
},
loadingLayer = new LoadingLayer(true),
}
},
new RoomInspector
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.Both,
Width = 0.45f,
},
}
},
},
filter = CreateFilterControl().With(d =>
{
d.RelativeSizeAxes = Axes.X;
d.Height = 80;
})
}
};
// scroll selected room into view on selection.
@ -116,18 +155,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
}
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
content.Padding = new MarginPadding
{
Top = filter.DrawHeight,
Left = WaveOverlayContainer.WIDTH_PADDING - DrawableRoom.SELECTION_BORDER_WIDTH + HORIZONTAL_OVERFLOW_PADDING,
Right = WaveOverlayContainer.WIDTH_PADDING + HORIZONTAL_OVERFLOW_PADDING,
};
}
protected override void OnFocus(FocusEvent e)
{
filter.TakeFocus();
@ -199,13 +226,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
/// <summary>
/// Push a room as a new subscreen.
/// </summary>
public void Open(Room room) => Schedule(() =>
/// <param name="room">An optional template to use when creating the room.</param>
public void Open(Room room = null) => Schedule(() =>
{
// Handles the case where a room is clicked 3 times in quick succession
if (!this.IsCurrentScreen())
return;
OpenNewRoom(room);
OpenNewRoom(room ?? CreateNewRoom());
});
protected virtual void OpenNewRoom(Room room)
@ -217,6 +245,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
protected abstract FilterControl CreateFilterControl();
protected abstract OsuButton CreateNewRoomButton();
/// <summary>
/// Creates a new room.
/// </summary>
/// <returns>The created <see cref="Room"/>.</returns>
protected abstract Room CreateNewRoom();
protected abstract RoomSubScreen CreateRoomSubScreen(Room room);
private void updateLoadingLayer()

View File

@ -12,6 +12,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
[BackgroundDependencyLoader]
private void load()
{
SpriteText.Font = SpriteText.Font.With(size: 14);
Triangles.TriangleScale = 1.5f;
}

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

@ -4,9 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge;
@ -54,20 +52,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Logger.Log($"Polling adjusted (listing: {multiplayerRoomManager.TimeBetweenListingPolls.Value}, selection: {multiplayerRoomManager.TimeBetweenSelectionPolls.Value})");
}
protected override Room CreateNewRoom() =>
new Room
{
Name = { Value = $"{API.LocalUser}'s awesome room" },
Category = { Value = RoomCategory.Realtime },
Type = { Value = MatchType.HeadToHead },
};
protected override string ScreenTitle => "Multiplayer";
protected override RoomManager CreateRoomManager() => new MultiplayerRoomManager();
protected override LoungeSubScreen CreateLounge() => new MultiplayerLoungeSubScreen();
protected override OsuButton CreateNewMultiplayerGameButton() => new CreateMultiplayerMatchButton();
}
}

View File

@ -3,6 +3,8 @@
using osu.Framework.Allocation;
using osu.Framework.Logging;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Lounge;
@ -13,13 +15,25 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
public class MultiplayerLoungeSubScreen : LoungeSubScreen
{
protected override FilterControl CreateFilterControl() => new MultiplayerFilterControl();
protected override RoomSubScreen CreateRoomSubScreen(Room room) => new MultiplayerMatchSubScreen(room);
[Resolved]
private IAPIProvider api { get; set; }
[Resolved]
private MultiplayerClient client { get; set; }
protected override FilterControl CreateFilterControl() => new MultiplayerFilterControl();
protected override OsuButton CreateNewRoomButton() => new CreateMultiplayerMatchButton();
protected override Room CreateNewRoom() => new Room
{
Name = { Value = $"{api.LocalUser}'s awesome room" },
Category = { Value = RoomCategory.Realtime },
Type = { Value = MatchType.HeadToHead },
};
protected override RoomSubScreen CreateRoomSubScreen(Room room) => new MultiplayerMatchSubScreen(room);
protected override void OpenNewRoom(Room room)
{
if (client?.IsConnected.Value != true)

View File

@ -475,16 +475,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override Screen CreateGameplayScreen()
{
Debug.Assert(client.LocalUser != null);
Debug.Assert(client.Room != null);
int[] userIds = client.CurrentMatchPlayingUserIds.ToArray();
MultiplayerRoomUser[] users = userIds.Select(id => client.Room.Users.First(u => u.UserID == id)).ToArray();
switch (client.LocalUser.State)
{
case MultiplayerUserState.Spectating:
return new MultiSpectatorScreen(userIds);
return new MultiSpectatorScreen(users.Take(PlayerGrid.MAX_PLAYERS).ToArray());
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.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
@ -34,16 +37,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private MultiplayerGameplayLeaderboard leaderboard;
private readonly int[] userIds;
private readonly MultiplayerRoomUser[] users;
private LoadingLayer loadingDisplay;
private FillFlowContainer leaderboardFlow;
/// <summary>
/// Construct a multiplayer player.
/// </summary>
/// <param name="playlistItem">The playlist item to be played.</param>
/// <param name="userIds">The users which are participating in this game.</param>
public MultiplayerPlayer(PlaylistItem playlistItem, int[] userIds)
/// <param name="users">The users which are participating in this game.</param>
public MultiplayerPlayer(PlaylistItem playlistItem, MultiplayerRoomUser[] users)
: base(playlistItem, new PlayerConfiguration
{
AllowPause = false,
@ -51,14 +55,41 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
AllowSkipping = false,
})
{
this.userIds = userIds;
this.users = users;
}
[BackgroundDependencyLoader]
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.
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 });
}
@ -67,6 +98,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
base.LoadAsyncComplete();
if (!LoadedBeatmapSuccessfully)
return;
if (!ValidForResume)
return; // token retrieval may have failed.
@ -92,13 +126,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Debug.Assert(client.Room != null);
}
protected override void LoadComplete()
{
base.LoadComplete();
((IBindable<bool>)leaderboard.Expanded).BindTo(HUDOverlay.ShowHud);
}
protected override void StartGameplay()
{
// 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()
{
base.Update();
if (!LoadedBeatmapSuccessfully)
return;
adjustLeaderboardPosition();
}
@ -125,7 +156,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
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(() =>

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
@ -42,6 +43,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
private ModDisplay userModsDisplay;
private StateDisplay userStateDisplay;
private IconButton kickButton;
public ParticipantPanel(MultiplayerRoomUser user)
{
User = user;
@ -64,7 +67,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
{
new Dimension(GridSizeMode.Absolute, 18),
new Dimension(GridSizeMode.AutoSize),
new Dimension()
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
@ -157,7 +161,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
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();
if (Room == null)
if (Room == null || Client.LocalUser == null)
return;
const double fade_time = 50;
@ -179,6 +196,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
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)
crown.FadeIn(fade_time);
else
@ -211,13 +233,36 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
new OsuMenuItem("Give host", MenuItemType.Standard, () =>
{
// Ensure the local user is still host.
if (Room.Host?.UserID != api.LocalUser.Value.Id)
if (!Client.IsHost)
return;
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 JetBrains.Annotations;
using osu.Framework.Timing;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
@ -11,8 +12,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
public class MultiSpectatorLeaderboard : MultiplayerGameplayLeaderboard
{
public MultiSpectatorLeaderboard([NotNull] ScoreProcessor scoreProcessor, int[] userIds)
: base(scoreProcessor, userIds)
public MultiSpectatorLeaderboard([NotNull] ScoreProcessor scoreProcessor, MultiplayerRoomUser[] users)
: base(scoreProcessor, users)
{
}
@ -32,7 +33,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
((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()
{
@ -47,8 +48,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
[CanBeNull]
public IClock Clock;
public SpectatingTrackedUserData(int userId, ScoreProcessor scoreProcessor)
: base(userId, scoreProcessor)
public SpectatingTrackedUserData(MultiplayerRoomUser user, ScoreProcessor scoreProcessor)
: base(user, scoreProcessor)
{
}

View File

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

@ -35,6 +35,9 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved(typeof(Room))]
protected BindableList<PlaylistItem> Playlist { get; private set; }
[Resolved(typeof(Room))]
protected Bindable<RoomCategory> Category { get; private set; }
[Resolved(typeof(Room))]
protected BindableList<User> RecentParticipants { get; private set; }

View File

@ -13,7 +13,6 @@ using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
@ -24,13 +23,15 @@ using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Screens.OnlinePlay
{
[Cached]
public abstract class OnlinePlayScreen : OsuScreen, IHasSubScreenStack
{
[Cached]
protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
public override bool CursorVisible => (screenStack?.CurrentScreen as IOnlinePlaySubScreen)?.CursorVisible ?? true;
// this is required due to PlayerLoader eventually being pushed to the main stack
@ -38,12 +39,8 @@ namespace osu.Game.Screens.OnlinePlay
public override bool DisallowExternalBeatmapRulesetChanges => true;
private MultiplayerWaveContainer waves;
private OsuButton createButton;
private ScreenStack screenStack;
private LoungeSubScreen loungeSubScreen;
private ScreenStack screenStack;
private readonly IBindable<bool> isIdle = new BindableBool();
@ -146,20 +143,8 @@ namespace osu.Game.Screens.OnlinePlay
}
},
new Header(ScreenTitle, screenStack),
createButton = CreateNewMultiplayerGameButton().With(button =>
{
button.Anchor = Anchor.TopRight;
button.Origin = Anchor.TopRight;
button.Size = new Vector2(150, Header.HEIGHT - 20);
button.Margin = new MarginPadding
{
Top = 10,
Right = 10 + HORIZONTAL_OVERFLOW_PADDING,
};
button.Action = () => OpenNewRoom();
}),
RoomManager,
ongoingOperationTracker,
ongoingOperationTracker
}
};
}
@ -292,18 +277,6 @@ namespace osu.Game.Screens.OnlinePlay
logo.Delay(WaveContainer.DISAPPEAR_DURATION / 2).FadeOut();
}
/// <summary>
/// Creates and opens the newly-created room.
/// </summary>
/// <param name="room">An optional template to use when creating the room.</param>
public void OpenNewRoom(Room room = null) => loungeSubScreen.Open(room ?? CreateNewRoom());
/// <summary>
/// Creates a new room.
/// </summary>
/// <returns>The created <see cref="Room"/>.</returns>
protected abstract Room CreateNewRoom();
private void screenPushed(IScreen lastScreen, IScreen newScreen)
{
subScreenChanged(lastScreen, newScreen);
@ -339,7 +312,6 @@ namespace osu.Game.Screens.OnlinePlay
((IBindable<UserActivity>)Activity).BindTo(newOsuScreen.Activity);
UpdatePollingRate(isIdle.Value);
createButton.FadeTo(newScreen is LoungeSubScreen ? 1 : 0, 200);
}
protected IScreen CurrentSubScreen => screenStack.CurrentScreen;
@ -350,8 +322,6 @@ namespace osu.Game.Screens.OnlinePlay
protected abstract LoungeSubScreen CreateLounge();
protected abstract OsuButton CreateNewMultiplayerGameButton();
private class MultiplayerWaveContainer : WaveContainer
{
protected override bool StartHidden => true;

View File

@ -3,8 +3,6 @@
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Match;
@ -46,21 +44,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
Logger.Log($"Polling adjusted (listing: {playlistsManager.TimeBetweenListingPolls.Value}, selection: {playlistsManager.TimeBetweenSelectionPolls.Value})");
}
protected override Room CreateNewRoom()
{
return new Room
{
Name = { Value = $"{API.LocalUser}'s awesome playlist" },
Type = { Value = MatchType.Playlists }
};
}
protected override string ScreenTitle => "Playlists";
protected override RoomManager CreateRoomManager() => new PlaylistsRoomManager();
protected override LoungeSubScreen CreateLounge() => new PlaylistsLoungeSubScreen();
protected override OsuButton CreateNewMultiplayerGameButton() => new CreatePlaylistsRoomButton();
}
}

View File

@ -1,6 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
@ -10,8 +13,22 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{
public class PlaylistsLoungeSubScreen : LoungeSubScreen
{
[Resolved]
private IAPIProvider api { get; set; }
protected override FilterControl CreateFilterControl() => new PlaylistsFilterControl();
protected override OsuButton CreateNewRoomButton() => new CreatePlaylistsRoomButton();
protected override Room CreateNewRoom()
{
return new Room
{
Name = { Value = $"{api.LocalUser}'s awesome playlist" },
Type = { Value = MatchType.Playlists }
};
}
protected override RoomSubScreen CreateRoomSubScreen(Room room) => new PlaylistsRoomSubScreen(room);
}
}

View File

@ -48,10 +48,9 @@ namespace osu.Game.Screens.Play.HUD
/// </param>
public ILeaderboardScore AddPlayer([CanBeNull] User user, bool isTracked)
{
var drawable = new GameplayLeaderboardScore(user, isTracked)
{
Expanded = { BindTarget = Expanded },
};
var drawable = CreateLeaderboardScoreDrawable(user, isTracked);
drawable.Expanded.BindTo(Expanded);
base.Add(drawable);
drawable.TotalScore.BindValueChanged(_ => sorting.Invalidate(), true);
@ -61,6 +60,9 @@ namespace osu.Game.Screens.Play.HUD
return drawable;
}
protected virtual GameplayLeaderboardScore CreateLeaderboardScoreDrawable(User user, bool isTracked) =>
new GameplayLeaderboardScore(user, isTracked);
public sealed override void Add(GameplayLeaderboardScore drawable)
{
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 BindableBool HasQuit { get; } = new BindableBool();
public Color4? BackgroundColour { get; set; }
public Color4? TextColour { get; set; }
private int? scorePosition;
public int? ScorePosition
@ -331,19 +335,19 @@ namespace osu.Game.Screens.Play.HUD
if (scorePosition == 1)
{
widthExtension = true;
panelColour = Color4Extensions.FromHex("7fcc33");
textColour = Color4.White;
panelColour = BackgroundColour ?? Color4Extensions.FromHex("7fcc33");
textColour = TextColour ?? Color4.White;
}
else if (trackedPlayer)
{
widthExtension = true;
panelColour = Color4Extensions.FromHex("ffd966");
textColour = Color4Extensions.FromHex("2e576b");
panelColour = BackgroundColour ?? Color4Extensions.FromHex("ffd966");
textColour = TextColour ?? Color4Extensions.FromHex("2e576b");
}
else
{
panelColour = Color4Extensions.FromHex("3399cc");
textColour = Color4.White;
panelColour = BackgroundColour ?? Color4Extensions.FromHex("3399cc");
textColour = TextColour ?? Color4.White;
}
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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Scoring;
using osu.Game.Users;
using osuTK.Graphics;
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>();
public readonly SortedDictionary<int, BindableInt> TeamScores = new SortedDictionary<int, BindableInt>();
[Resolved]
private OsuColour colours { get; set; }
[Resolved]
private SpectatorClient spectatorClient { get; set; }
@ -31,21 +41,24 @@ namespace osu.Game.Screens.Play.HUD
private UserLookupCache userLookupCache { get; set; }
private readonly ScoreProcessor scoreProcessor;
private readonly IBindableList<int> playingUsers;
private readonly MultiplayerRoomUser[] playingUsers;
private Bindable<ScoringMode> scoringMode;
private readonly IBindableList<int> playingUserIds = new BindableList<int>();
private bool hasTeams => TeamScores.Count > 0;
/// <summary>
/// Construct a new leaderboard.
/// </summary>
/// <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>
public MultiplayerGameplayLeaderboard(ScoreProcessor scoreProcessor, int[] userIds)
/// <param name="users">IDs of all users in this match.</param>
public MultiplayerGameplayLeaderboard(ScoreProcessor scoreProcessor, MultiplayerRoomUser[] users)
{
// todo: this will eventually need to be created per user to support different mod combinations.
this.scoreProcessor = scoreProcessor;
// todo: this will likely be passed in as User instances.
playingUsers = new BindableList<int>(userIds);
playingUsers = users;
}
[BackgroundDependencyLoader]
@ -53,14 +66,17 @@ namespace osu.Game.Screens.Play.HUD
{
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);
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)
{
@ -83,23 +99,50 @@ namespace osu.Game.Screens.Play.HUD
base.LoadComplete();
// 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))
usersChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { userId }));
if (!multiplayerClient.CurrentMatchPlayingUserIds.Contains(user.UserID))
usersChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { user.UserID }));
}
// bind here is to support players leaving the match.
// new players are not supported.
playingUsers.BindTo(multiplayerClient.CurrentMatchPlayingUserIds);
playingUsers.BindCollectionChanged(usersChanged);
playingUserIds.BindTo(multiplayerClient.CurrentMatchPlayingUserIds);
playingUserIds.BindCollectionChanged(usersChanged);
// this leaderboard should be guaranteed to be completely loaded before the gameplay starts (is a prerequisite in MultiplayerPlayer).
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)
{
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.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)
{
@ -136,7 +196,7 @@ namespace osu.Game.Screens.Play.HUD
{
foreach (var user in playingUsers)
{
spectatorClient.StopWatchingUser(user);
spectatorClient.StopWatchingUser(user.UserID);
}
spectatorClient.OnNewFrames -= handleIncomingFrames;
@ -145,7 +205,7 @@ namespace osu.Game.Screens.Play.HUD
protected class TrackedUserData
{
public readonly int UserId;
public readonly MultiplayerRoomUser User;
public readonly ScoreProcessor ScoreProcessor;
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 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;
ScoringMode.BindValueChanged(_ => UpdateScore());

View File

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

View File

@ -36,24 +36,29 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
if (joinRoom)
{
var room = new Room
{
Name = { Value = "test name" },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo },
Ruleset = { Value = Ruleset.Value }
}
}
};
var room = CreateRoom();
RoomManager.CreateRoom(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()
{
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 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)
{
Debug.Assert(Room != null);

View File

@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
@ -46,6 +47,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay
CacheAs(Filter);
CacheAs(OngoingOperationTracker);
CacheAs(AvailabilityTracker);
CacheAs(new OverlayColourProvider(OverlayColourScheme.Plum));
}
public object Get(Type type)