diff --git a/osu.Game.Tests/Visual/Components/TestScenePollingComponent.cs b/osu.Game.Tests/Visual/Components/TestScenePollingComponent.cs index fb10015ef4..2236f85b92 100644 --- a/osu.Game.Tests/Visual/Components/TestScenePollingComponent.cs +++ b/osu.Game.Tests/Visual/Components/TestScenePollingComponent.cs @@ -61,12 +61,12 @@ namespace osu.Game.Tests.Visual.Components { createPoller(true); - AddStep("set poll interval to 1", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust); + AddStep("set poll interval to 1", () => poller.TimeBetweenPolls.Value = TimePerAction * safety_adjust); checkCount(1); checkCount(2); checkCount(3); - AddStep("set poll interval to 5", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust * 5); + AddStep("set poll interval to 5", () => poller.TimeBetweenPolls.Value = TimePerAction * safety_adjust * 5); checkCount(4); checkCount(4); checkCount(4); @@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.Components checkCount(5); checkCount(5); - AddStep("set poll interval to 1", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust); + AddStep("set poll interval to 1", () => poller.TimeBetweenPolls.Value = TimePerAction * safety_adjust); checkCount(6); checkCount(7); } @@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.Components { createPoller(false); - AddStep("set poll interval to 1", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust * 5); + AddStep("set poll interval to 1", () => poller.TimeBetweenPolls.Value = TimePerAction * safety_adjust * 5); checkCount(0); skip(); checkCount(0); @@ -141,7 +141,7 @@ namespace osu.Game.Tests.Visual.Components public class TestSlowPoller : TestPoller { - protected override Task Poll() => Task.Delay((int)(TimeBetweenPolls / 2f / Clock.Rate)).ContinueWith(_ => base.Poll()); + protected override Task Poll() => Task.Delay((int)(TimeBetweenPolls.Value / 2f / Clock.Rate)).ContinueWith(_ => base.Poll()); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs index 6fd5511e5a..4ee48fd853 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs @@ -10,6 +10,8 @@ using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Scoring; +using osu.Game.Screens.Ranking; using osu.Game.Storyboards; using osuTK; @@ -50,7 +52,7 @@ namespace osu.Game.Tests.Visual.Gameplay cancel(); complete(); - AddUntilStep("attempted to push ranking", () => ((FakeRankingPushPlayer)Player).GotoRankingInvoked); + AddUntilStep("attempted to push ranking", () => ((FakeRankingPushPlayer)Player).ResultsCreated); } /// @@ -84,7 +86,7 @@ namespace osu.Game.Tests.Visual.Gameplay { // wait to ensure there was no attempt of pushing the results screen. AddWaitStep("wait", resultsDisplayWaitCount); - AddAssert("no attempt to push ranking", () => !((FakeRankingPushPlayer)Player).GotoRankingInvoked); + AddAssert("no attempt to push ranking", () => !((FakeRankingPushPlayer)Player).ResultsCreated); } protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) @@ -110,16 +112,18 @@ namespace osu.Game.Tests.Visual.Gameplay public class FakeRankingPushPlayer : TestPlayer { - public bool GotoRankingInvoked; + public bool ResultsCreated { get; private set; } public FakeRankingPushPlayer() : base(true, true) { } - protected override void GotoRanking() + protected override ResultsScreen CreateResults(ScoreInfo score) { - GotoRankingInvoked = true; + var results = base.CreateResults(score); + ResultsCreated = true; + return results; } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index df970c1c46..ca61672ef9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Screens.Play.HUD; using osu.Game.Users; using osuTK; @@ -26,7 +27,6 @@ namespace osu.Game.Tests.Visual.Gameplay Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(2), - RelativeSizeAxes = Axes.X, }); } @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay playerScore.Value = 1222333; }); - AddStep("add player user", () => leaderboard.AddPlayer(playerScore, new User { Username = "You" })); + AddStep("add local player", () => createLeaderboardScore(playerScore, new User { Username = "You", Id = 3 }, true)); AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v); } @@ -49,8 +49,8 @@ namespace osu.Game.Tests.Visual.Gameplay var player2Score = new BindableDouble(1234567); var player3Score = new BindableDouble(1111111); - AddStep("add player 2", () => leaderboard.AddPlayer(player2Score, new User { Username = "Player 2" })); - AddStep("add player 3", () => leaderboard.AddPlayer(player3Score, new User { Username = "Player 3" })); + AddStep("add player 2", () => createLeaderboardScore(player2Score, new User { Username = "Player 2" })); + AddStep("add player 3", () => createLeaderboardScore(player3Score, new User { Username = "Player 3" })); AddAssert("is player 2 position #1", () => leaderboard.CheckPositionByUsername("Player 2", 1)); AddAssert("is player position #2", () => leaderboard.CheckPositionByUsername("You", 2)); @@ -67,6 +67,30 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("is player 2 position #3", () => leaderboard.CheckPositionByUsername("Player 2", 3)); } + [Test] + public void TestRandomScores() + { + int playerNumber = 1; + AddRepeatStep("add player with random score", () => createRandomScore(new User { Username = $"Player {playerNumber++}" }), 10); + } + + [Test] + public void TestExistingUsers() + { + AddStep("add peppy", () => createRandomScore(new User { Username = "peppy", Id = 2 })); + AddStep("add smoogipoo", () => createRandomScore(new User { Username = "smoogipoo", Id = 1040328 })); + AddStep("add flyte", () => createRandomScore(new User { Username = "flyte", Id = 3103765 })); + AddStep("add frenzibyte", () => createRandomScore(new User { Username = "frenzibyte", Id = 14210502 })); + } + + private void createRandomScore(User user) => createLeaderboardScore(new BindableDouble(RNG.Next(0, 5_000_000)), user); + + private void createLeaderboardScore(BindableDouble score, User user, bool isTracked = false) + { + var leaderboardScore = leaderboard.AddPlayer(user, isTracked); + leaderboardScore.TotalScore.BindTo(score); + } + private class TestGameplayLeaderboard : GameplayLeaderboard { public bool CheckPositionByUsername(string username, int? expectedPosition) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs new file mode 100644 index 0000000000..e42ddeb35e --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs @@ -0,0 +1,155 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Database; +using osu.Game.Online.Spectator; +using osu.Game.Replays.Legacy; +using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Play.HUD; +using osu.Game.Tests.Visual.Online; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneMultiplayerGameplayLeaderboard : OsuTestScene + { + [Cached(typeof(SpectatorStreamingClient))] + private TestMultiplayerStreaming streamingClient = new TestMultiplayerStreaming(16); + + [Cached(typeof(UserLookupCache))] + private UserLookupCache lookupCache = new TestSceneCurrentlyPlayingDisplay.TestUserLookupCache(); + + private MultiplayerGameplayLeaderboard leaderboard; + + protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; + + public TestSceneMultiplayerGameplayLeaderboard() + { + base.Content.Children = new Drawable[] + { + streamingClient, + lookupCache, + Content + }; + } + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("create leaderboard", () => + { + OsuScoreProcessor scoreProcessor; + Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); + + var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); + + streamingClient.Start(Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0); + + Children = new Drawable[] + { + scoreProcessor = new OsuScoreProcessor(), + }; + + scoreProcessor.ApplyBeatmap(playable); + + LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, streamingClient.PlayingUsers.ToArray()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, Add); + }); + + AddUntilStep("wait for load", () => leaderboard.IsLoaded); + } + + [Test] + public void TestScoreUpdates() + { + AddRepeatStep("update state", () => streamingClient.RandomlyUpdateState(), 100); + } + + public class TestMultiplayerStreaming : SpectatorStreamingClient + { + public new BindableList PlayingUsers => (BindableList)base.PlayingUsers; + + private readonly int totalUsers; + + public TestMultiplayerStreaming(int totalUsers) + { + this.totalUsers = totalUsers; + } + + public void Start(int beatmapId) + { + for (int i = 0; i < totalUsers; i++) + { + ((ISpectatorClient)this).UserBeganPlaying(i, new SpectatorState + { + BeatmapID = beatmapId, + RulesetID = 0, + }); + } + } + + private readonly Dictionary lastHeaders = new Dictionary(); + + public void RandomlyUpdateState() + { + foreach (var userId in PlayingUsers) + { + if (RNG.NextBool()) + continue; + + if (!lastHeaders.TryGetValue(userId, out var header)) + { + lastHeaders[userId] = header = new FrameHeader(new ScoreInfo + { + Statistics = new Dictionary + { + [HitResult.Miss] = 0, + [HitResult.Meh] = 0, + [HitResult.Great] = 0 + } + }); + } + + switch (RNG.Next(0, 3)) + { + case 0: + header.Combo = 0; + header.Statistics[HitResult.Miss]++; + break; + + case 1: + header.Combo++; + header.MaxCombo = Math.Max(header.MaxCombo, header.Combo); + header.Statistics[HitResult.Meh]++; + break; + + default: + header.Combo++; + header.MaxCombo = Math.Max(header.MaxCombo, header.Combo); + header.Statistics[HitResult.Great]++; + break; + } + + ((ISpectatorClient)this).UserSentFrames(userId, new FrameDataBundle(header, Array.Empty())); + } + } + + protected override Task Connect() => Task.CompletedTask; + } + } +} diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 3e5b561a6f..1fdff99da6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -232,7 +232,7 @@ namespace osu.Game.Tests.Visual.Gameplay public class TestSpectatorStreamingClient : SpectatorStreamingClient { - public readonly User StreamingUser = new User { Id = 1234, Username = "Test user" }; + public readonly User StreamingUser = new User { Id = 55, Username = "Test user" }; public new BindableList PlayingUsers => (BindableList)base.PlayingUsers; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs index 67a53307fc..9dd4aea4bd 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs @@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public readonly BindableList Rooms = new BindableList(); - public Bindable InitialRoomsReceived { get; } = new Bindable(true); + public IBindable InitialRoomsReceived { get; } = new Bindable(true); IBindableList IRoomManager.Rooms => Rooms; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs index cbe8cc6137..234374ee2b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs @@ -131,7 +131,7 @@ namespace osu.Game.Tests.Visual.Multiplayer remove { } } - public Bindable InitialRoomsReceived { get; } = new Bindable(true); + public IBindable InitialRoomsReceived { get; } = new Bindable(true); public IBindableList Rooms => null; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs index 65e9893851..bceb6efac1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs @@ -151,7 +151,7 @@ namespace osu.Game.Tests.Visual.Multiplayer remove => throw new NotImplementedException(); } - public Bindable InitialRoomsReceived { get; } = new Bindable(true); + public IBindable InitialRoomsReceived { get; } = new Bindable(true); public IBindableList Rooms { get; } = new BindableList(); diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs index 7eba64f418..1666c9cde4 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs @@ -69,8 +69,32 @@ namespace osu.Game.Tests.Visual.Online internal class TestUserLookupCache : UserLookupCache { + private static readonly string[] usernames = + { + "fieryrage", + "Kerensa", + "MillhioreF", + "Player01", + "smoogipoo", + "Ephemeral", + "BTMC", + "Cilvery", + "m980", + "HappyStick", + "LittleEndu", + "frenzibyte", + "Zallius", + "BanchoBot", + "rocketminer210", + "pishifat" + }; + protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) - => Task.FromResult(new User { Username = "peppy", Id = 2 }); + => Task.FromResult(new User + { + Id = lookup, + Username = usernames[lookup % usernames.Length], + }); } } } diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 16f46581c5..62ae507419 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -57,7 +57,7 @@ namespace osu.Game.Online.Chat { CurrentChannel.ValueChanged += currentChannelChanged; - HighPollRate.BindValueChanged(enabled => TimeBetweenPolls = enabled.NewValue ? 1000 : 6000, true); + HighPollRate.BindValueChanged(enabled => TimeBetweenPolls.Value = enabled.NewValue ? 1000 : 6000, true); } /// diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index dcd0cb435a..d8207aa8f4 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -24,8 +24,8 @@ using osu.Game.Scoring; using osu.Game.Users.Drawables; using osuTK; using osuTK.Graphics; -using Humanizer; using osu.Game.Online.API; +using osu.Game.Utils; namespace osu.Game.Online.Leaderboards { @@ -358,7 +358,7 @@ namespace osu.Game.Online.Leaderboards Anchor = Anchor.Centre, Origin = Anchor.Centre, Font = OsuFont.GetFont(size: 20, italics: true), - Text = rank == null ? "-" : rank.Value.ToMetric(decimals: rank < 100000 ? 1 : 0), + Text = rank == null ? "-" : rank.Value.FormatRank() }; } diff --git a/osu.Game/Online/PollingComponent.cs b/osu.Game/Online/PollingComponent.cs index 228f147835..3d19f2ab09 100644 --- a/osu.Game/Online/PollingComponent.cs +++ b/osu.Game/Online/PollingComponent.cs @@ -3,6 +3,7 @@ using System; using System.Threading.Tasks; +using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Framework.Threading; @@ -19,22 +20,11 @@ namespace osu.Game.Online private bool pollingActive; - private double timeBetweenPolls; - /// /// The time in milliseconds to wait between polls. /// Setting to zero stops all polling. /// - public double TimeBetweenPolls - { - get => timeBetweenPolls; - set - { - timeBetweenPolls = value; - scheduledPoll?.Cancel(); - pollIfNecessary(); - } - } + public readonly Bindable TimeBetweenPolls = new Bindable(); /// /// @@ -42,7 +32,13 @@ namespace osu.Game.Online /// The initial time in milliseconds to wait between polls. Setting to zero stops all polling. protected PollingComponent(double timeBetweenPolls = 0) { - TimeBetweenPolls = timeBetweenPolls; + TimeBetweenPolls.BindValueChanged(_ => + { + scheduledPoll?.Cancel(); + pollIfNecessary(); + }); + + TimeBetweenPolls.Value = timeBetweenPolls; } protected override void LoadComplete() @@ -60,7 +56,7 @@ namespace osu.Game.Online if (pollingActive) return false; // don't try polling if the time between polls hasn't been set. - if (timeBetweenPolls == 0) return false; + if (TimeBetweenPolls.Value == 0) return false; if (!lastTimePolled.HasValue) { @@ -68,7 +64,7 @@ namespace osu.Game.Online return true; } - if (Time.Current - lastTimePolled.Value > timeBetweenPolls) + if (Time.Current - lastTimePolled.Value > TimeBetweenPolls.Value) { doPoll(); return true; @@ -99,7 +95,7 @@ namespace osu.Game.Online /// public void PollImmediately() { - lastTimePolled = Time.Current - timeBetweenPolls; + lastTimePolled = Time.Current - TimeBetweenPolls.Value; scheduleNextPoll(); } @@ -121,7 +117,7 @@ namespace osu.Game.Online double lastPollDuration = lastTimePolled.HasValue ? Time.Current - lastTimePolled.Value : 0; - scheduledPoll = Scheduler.AddDelayed(doPoll, Math.Max(0, timeBetweenPolls - lastPollDuration)); + scheduledPoll = Scheduler.AddDelayed(doPoll, Math.Max(0, TimeBetweenPolls.Value - lastPollDuration)); } } } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 499673619f..10d0cc2865 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -68,7 +68,12 @@ namespace osu.Game.Rulesets.Scoring private readonly double comboPortion; private int maxAchievableCombo; + + /// + /// The maximum achievable base score. + /// private double maxBaseScore; + private double rollingMaxBaseScore; private double baseScore; @@ -196,8 +201,8 @@ namespace osu.Game.Rulesets.Scoring private double getScore(ScoringMode mode) { return GetScore(mode, maxAchievableCombo, - maxBaseScore > 0 ? baseScore / maxBaseScore : 0, - maxAchievableCombo > 0 ? (double)HighestCombo.Value / maxAchievableCombo : 1, + calculateAccuracyRatio(baseScore), + calculateComboRatio(HighestCombo.Value), scoreResultCounts); } @@ -227,6 +232,37 @@ namespace osu.Game.Rulesets.Scoring } } + /// + /// Given a minimal set of inputs, return the computed score and accuracy for the tracked beatmap / mods combination. + /// + /// The to compute the total score in. + /// The maximum combo achievable in the beatmap. + /// Statistics to be used for calculating accuracy, bonus score, etc. + /// The computed score and accuracy for provided inputs. + public (double score, double accuracy) GetScoreAndAccuracy(ScoringMode mode, int maxCombo, Dictionary statistics) + { + // calculate base score from statistics pairs + int computedBaseScore = 0; + + foreach (var pair in statistics) + { + if (!pair.Key.AffectsAccuracy()) + continue; + + computedBaseScore += Judgement.ToNumericResult(pair.Key) * pair.Value; + } + + double accuracy = calculateAccuracyRatio(computedBaseScore); + double comboRatio = calculateComboRatio(maxCombo); + + double score = GetScore(mode, maxAchievableCombo, accuracy, comboRatio, scoreResultCounts); + + return (score, accuracy); + } + + private double calculateAccuracyRatio(double baseScore) => maxBaseScore > 0 ? baseScore / maxBaseScore : 0; + private double calculateComboRatio(int maxCombo) => maxAchievableCombo > 0 ? (double)maxCombo / maxAchievableCombo : 1; + private double getBonusScore(Dictionary statistics) => statistics.GetOrDefault(HitResult.SmallBonus) * Judgement.SMALL_BONUS_SCORE + statistics.GetOrDefault(HitResult.LargeBonus) * Judgement.LARGE_BONUS_SCORE; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 347d9e3ba7..2f4721f63e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -92,6 +92,7 @@ namespace osu.Game.Screens.Edit.Compose.Components } } + private Container dragHandles; private FillFlowContainer buttons; public const float BORDER_RADIUS = 3; @@ -151,6 +152,12 @@ namespace osu.Game.Screens.Edit.Compose.Components }, } }, + dragHandles = new Container + { + RelativeSizeAxes = Axes.Both, + // ensures that the centres of all drag handles line up with the middle of the selection box border. + Padding = new MarginPadding(BORDER_RADIUS / 2) + }, buttons = new FillFlowContainer { Y = 20, @@ -232,7 +239,7 @@ namespace osu.Game.Screens.Edit.Compose.Components }); } - private void addDragHandle(Anchor anchor) => AddInternal(new SelectionBoxDragHandle + private void addDragHandle(Anchor anchor) => dragHandles.Add(new SelectionBoxDragHandle { Anchor = anchor, HandleDrag = e => OnScale?.Invoke(e.Delta, anchor), diff --git a/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs b/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs index e22f09779e..dff6c50bf2 100644 --- a/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs +++ b/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs @@ -25,6 +25,7 @@ namespace osu.Game.Screens.Multi.Components { currentFilter.BindValueChanged(_ => { + NotifyRoomsReceived(null); if (IsLoaded) PollImmediately(); }); diff --git a/osu.Game/Screens/Multi/Components/RoomManager.cs b/osu.Game/Screens/Multi/Components/RoomManager.cs index 7b419c9efe..0bcf3a90c3 100644 --- a/osu.Game/Screens/Multi/Components/RoomManager.cs +++ b/osu.Game/Screens/Multi/Components/RoomManager.cs @@ -23,7 +23,8 @@ namespace osu.Game.Screens.Multi.Components private readonly BindableList rooms = new BindableList(); - public Bindable InitialRoomsReceived { get; } = new Bindable(); + public IBindable InitialRoomsReceived => initialRoomsReceived; + private readonly Bindable initialRoomsReceived = new Bindable(); public IBindableList Rooms => rooms; @@ -45,7 +46,6 @@ namespace osu.Game.Screens.Multi.Components InternalChildren = CreatePollingComponents().Select(p => { - p.InitialRoomsReceived.BindTo(InitialRoomsReceived); p.RoomsReceived = onRoomsReceived; return p; }).ToList(); @@ -123,6 +123,13 @@ namespace osu.Game.Screens.Multi.Components private void onRoomsReceived(List received) { + if (received == null) + { + rooms.Clear(); + initialRoomsReceived.Value = false; + return; + } + // Remove past matches foreach (var r in rooms.ToList()) { @@ -156,6 +163,7 @@ namespace osu.Game.Screens.Multi.Components } RoomsUpdated?.Invoke(); + initialRoomsReceived.Value = true; } protected void RemoveRoom(Room room) => rooms.Remove(room); @@ -192,6 +200,6 @@ namespace osu.Game.Screens.Multi.Components existing.CopyFrom(room); } - protected abstract RoomPollingComponent[] CreatePollingComponents(); + protected abstract IEnumerable CreatePollingComponents(); } } diff --git a/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs b/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs index 5430d54644..fbaf9dd930 100644 --- a/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs +++ b/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; @@ -13,29 +12,18 @@ namespace osu.Game.Screens.Multi.Components { public abstract class RoomPollingComponent : PollingComponent { - public Action> RoomsReceived; - /// - /// The time in milliseconds to wait between polls. - /// Setting to zero stops all polling. + /// Invoked when any s have been received from the API. + /// + /// Any s present locally but not returned by this event are to be removed from display. + /// If null, the display of local rooms is reset to an initial state. + /// /// - public new readonly Bindable TimeBetweenPolls = new Bindable(); - - public IBindable InitialRoomsReceived => initialRoomsReceived; - private readonly Bindable initialRoomsReceived = new Bindable(); + public Action> RoomsReceived; [Resolved] protected IAPIProvider API { get; private set; } - protected RoomPollingComponent() - { - TimeBetweenPolls.BindValueChanged(time => base.TimeBetweenPolls = time.NewValue); - } - - protected void NotifyRoomsReceived(List rooms) - { - initialRoomsReceived.Value = true; - RoomsReceived?.Invoke(rooms); - } + protected void NotifyRoomsReceived(List rooms) => RoomsReceived?.Invoke(rooms); } } diff --git a/osu.Game/Screens/Multi/IRoomManager.cs b/osu.Game/Screens/Multi/IRoomManager.cs index 3d18edcd71..630e3af91c 100644 --- a/osu.Game/Screens/Multi/IRoomManager.cs +++ b/osu.Game/Screens/Multi/IRoomManager.cs @@ -19,7 +19,7 @@ namespace osu.Game.Screens.Multi /// /// Whether an initial listing of rooms has been received. /// - Bindable InitialRoomsReceived { get; } + IBindable InitialRoomsReceived { get; } /// /// All the active s. diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs index a26a64d86d..165a2b201c 100644 --- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.Multi.Lounge protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby(); - private readonly Bindable initialRoomsReceived = new Bindable(); + private readonly IBindable initialRoomsReceived = new Bindable(); private Container content; private LoadingLayer loadingLayer; diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index 5f61c5e635..837ccdf2e9 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -46,7 +46,7 @@ namespace osu.Game.Screens.Multi private readonly IBindable isIdle = new BindableBool(); [Cached(Type = typeof(IRoomManager))] - protected IRoomManager RoomManager { get; private set; } + protected RoomManager RoomManager { get; private set; } [Cached] private readonly Bindable selectedRoom = new Bindable(); @@ -81,7 +81,7 @@ namespace osu.Game.Screens.Multi InternalChild = waves = new MultiplayerWaveContainer { RelativeSizeAxes = Axes.Both, - Children = new[] + Children = new Drawable[] { new Box { @@ -136,7 +136,7 @@ namespace osu.Game.Screens.Multi Origin = Anchor.TopRight, Action = () => CreateRoom() }, - (Drawable)(RoomManager = CreateRoomManager()) + RoomManager = CreateRoomManager() } }; @@ -353,7 +353,7 @@ namespace osu.Game.Screens.Multi } } - protected abstract IRoomManager CreateRoomManager(); + protected abstract RoomManager CreateRoomManager(); private class MultiplayerWaveContainer : WaveContainer { diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs index 0efa9c5196..41dcf61740 100644 --- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs +++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using System.Linq; using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Logging; @@ -95,19 +96,36 @@ namespace osu.Game.Screens.Multi.Play return new TimeshiftResultsScreen(score, roomId.Value.Value, playlistItem, true); } - protected override ScoreInfo CreateScore() + protected override Score CreateScore() { var score = base.CreateScore(); - score.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore()); + score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore()); + return score; + } + + protected override async Task SubmitScore(Score score) + { + await base.SubmitScore(score); Debug.Assert(token != null); - var request = new SubmitRoomScoreRequest(token.Value, roomId.Value ?? 0, playlistItem.ID, score); - request.Success += s => score.OnlineScoreID = s.ID; - request.Failure += e => Logger.Error(e, "Failed to submit score"); - api.Queue(request); + var tcs = new TaskCompletionSource(); + var request = new SubmitRoomScoreRequest(token.Value, roomId.Value ?? 0, playlistItem.ID, score.ScoreInfo); - return score; + request.Success += s => + { + score.ScoreInfo.OnlineScoreID = s.ID; + tcs.SetResult(true); + }; + + request.Failure += e => + { + Logger.Error(e, "Failed to submit score"); + tcs.SetResult(false); + }; + + api.Queue(request); + await tcs.Task; } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs index 1ff9c670a8..d2d6a35a2e 100644 --- a/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs @@ -3,6 +3,7 @@ using osu.Framework.Logging; using osu.Framework.Screens; +using osu.Game.Screens.Multi.Components; using osu.Game.Screens.Multi.Lounge; using osu.Game.Screens.Multi.Match; @@ -43,6 +44,6 @@ namespace osu.Game.Screens.Multi.Timeshift Logger.Log($"Polling adjusted (listing: {timeshiftManager.TimeBetweenListingPolls.Value}, selection: {timeshiftManager.TimeBetweenSelectionPolls.Value})"); } - protected override IRoomManager CreateRoomManager() => new TimeshiftRoomManager(); + protected override RoomManager CreateRoomManager() => new TimeshiftRoomManager(); } } diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomManager.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomManager.cs index ba96721afc..d21f844e04 100644 --- a/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomManager.cs +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomManager.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using osu.Framework.Bindables; using osu.Game.Screens.Multi.Components; @@ -11,7 +12,7 @@ namespace osu.Game.Screens.Multi.Timeshift public readonly Bindable TimeBetweenListingPolls = new Bindable(); public readonly Bindable TimeBetweenSelectionPolls = new Bindable(); - protected override RoomPollingComponent[] CreatePollingComponents() => new RoomPollingComponent[] + protected override IEnumerable CreatePollingComponents() => new RoomPollingComponent[] { new ListingPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls } }, new SelectionPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenSelectionPolls } } diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index e53c56b390..cab1cbd3f1 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -1,9 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; -using JetBrains.Annotations; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Users; @@ -15,8 +14,7 @@ namespace osu.Game.Screens.Play.HUD { public GameplayLeaderboard() { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; + Width = GameplayLeaderboardScore.EXTENDED_WIDTH + GameplayLeaderboardScore.SHEAR_WIDTH; Direction = FillDirection.Vertical; @@ -29,32 +27,35 @@ namespace osu.Game.Screens.Play.HUD /// /// Adds a player to the leaderboard. /// - /// The bindable current score of the player. /// The player. - public void AddPlayer([NotNull] BindableDouble currentScore, [NotNull] User user) + /// + /// Whether the player should be tracked on the leaderboard. + /// Set to true for the local player or a player whose replay is currently being played. + /// + public ILeaderboardScore AddPlayer(User user, bool isTracked) { - var scoreItem = addScore(currentScore.Value, user); - currentScore.ValueChanged += s => scoreItem.TotalScore = s.NewValue; - } - - private GameplayLeaderboardScore addScore(double totalScore, User user) - { - var scoreItem = new GameplayLeaderboardScore + var drawable = new GameplayLeaderboardScore(user, isTracked) { - User = user, - TotalScore = totalScore, - OnScoreChange = updateScores, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, }; - Add(scoreItem); - updateScores(); + base.Add(drawable); + drawable.TotalScore.BindValueChanged(_ => Scheduler.AddOnce(sort), true); - return scoreItem; + Height = Count * (GameplayLeaderboardScore.PANEL_HEIGHT + Spacing.Y); + + return drawable; } - private void updateScores() + public sealed override void Add(GameplayLeaderboardScore drawable) { - var orderedByScore = this.OrderByDescending(i => i.TotalScore).ToList(); + throw new NotSupportedException($"Use {nameof(AddPlayer)} instead."); + } + + private void sort() + { + var orderedByScore = this.OrderByDescending(i => i.TotalScore.Value).ToList(); for (int i = 0; i < Count; i++) { diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 4c75f422c9..58281debf1 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -1,25 +1,39 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using Humanizer; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; 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.Users; +using osu.Game.Users.Drawables; +using osu.Game.Utils; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { - public class GameplayLeaderboardScore : CompositeDrawable + public class GameplayLeaderboardScore : CompositeDrawable, ILeaderboardScore { - private readonly OsuSpriteText positionText, positionSymbol, userString; - private readonly GlowingSpriteText scoreText; + public const float EXTENDED_WIDTH = 255f; - public Action OnScoreChange; + private const float regular_width = 235f; + + public const float PANEL_HEIGHT = 35f; + + public const float SHEAR_WIDTH = PANEL_HEIGHT * panel_shear; + + private const float panel_shear = 0.15f; + + private OsuSpriteText positionText, scoreText, accuracyText, comboText, usernameText; + + public BindableDouble TotalScore { get; } = new BindableDouble(); + public BindableDouble Accuracy { get; } = new BindableDouble(1); + public BindableInt Combo { get; } = new BindableInt(); private int? scorePosition; @@ -28,109 +42,249 @@ namespace osu.Game.Screens.Play.HUD get => scorePosition; set { + if (value == scorePosition) + return; + scorePosition = value; if (scorePosition.HasValue) - positionText.Text = $"#{scorePosition.Value.ToMetric(decimals: scorePosition < 100000 ? 1 : 0)}"; + positionText.Text = $"#{scorePosition.Value.FormatRank()}"; positionText.FadeTo(scorePosition.HasValue ? 1 : 0); - positionSymbol.FadeTo(scorePosition.HasValue ? 1 : 0); + updateColour(); } } - private double totalScore; + public User User { get; } - public double TotalScore + private readonly bool trackedPlayer; + + private Container mainFillContainer; + private Box centralFill; + + /// + /// Creates a new . + /// + /// The score's player. + /// Whether the player is the local user or a replay player. + public GameplayLeaderboardScore(User user, bool trackedPlayer) { - get => totalScore; - set - { - totalScore = value; - scoreText.Text = totalScore.ToString("N0"); + User = user; + this.trackedPlayer = trackedPlayer; - OnScoreChange?.Invoke(); - } - } - - private User user; - - public User User - { - get => user; - set - { - user = value; - userString.Text = user?.Username; - } - } - - public GameplayLeaderboardScore() - { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - - InternalChild = new Container - { - Masking = true, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - new FillFlowContainer - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Right = 2.5f }, - Spacing = new Vector2(2.5f), - Children = new[] - { - positionText = new OsuSpriteText - { - Alpha = 0, - Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold), - }, - positionSymbol = new OsuSpriteText - { - Alpha = 0, - Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), - Text = ">", - }, - } - }, - new FillFlowContainer - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopLeft, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Left = 2.5f }, - Spacing = new Vector2(2.5f), - Children = new Drawable[] - { - userString = new OsuSpriteText - { - Size = new Vector2(80, 16), - Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, italics: true), - }, - scoreText = new GlowingSpriteText - { - GlowColour = Color4Extensions.FromHex(@"83ccfa"), - Font = OsuFont.Numeric.With(size: 14), - } - } - }, - }, - }; + Size = new Vector2(EXTENDED_WIDTH, PANEL_HEIGHT); } [BackgroundDependencyLoader] private void load(OsuColour colours) { - positionText.Colour = colours.YellowLight; - positionSymbol.Colour = colours.Yellow; + InternalChildren = new Drawable[] + { + mainFillContainer = new Container + { + Width = regular_width, + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Masking = true, + CornerRadius = 5f, + Shear = new Vector2(panel_shear, 0f), + Child = new Box + { + Alpha = 0.5f, + RelativeSizeAxes = Axes.Both, + } + }, + new GridContainer + { + Width = regular_width, + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 35f), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 85f), + }, + Content = new[] + { + new Drawable[] + { + positionText = new OsuSpriteText + { + Padding = new MarginPadding { Right = SHEAR_WIDTH / 2 }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.White, + Font = OsuFont.Torus.With(size: 14, weight: FontWeight.Bold), + Shadow = false, + }, + new Container + { + Padding = new MarginPadding { Horizontal = SHEAR_WIDTH / 3 }, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Container + { + Masking = true, + CornerRadius = 5f, + Shear = new Vector2(panel_shear, 0f), + RelativeSizeAxes = Axes.Both, + Children = new[] + { + centralFill = new Box + { + Alpha = 0.5f, + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("3399cc"), + }, + } + }, + new FillFlowContainer + { + Padding = new MarginPadding { Left = SHEAR_WIDTH }, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4f, 0f), + Children = new Drawable[] + { + new CircularContainer + { + Masking = true, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(25f), + Children = new Drawable[] + { + new Box + { + Name = "Placeholder while avatar loads", + Alpha = 0.3f, + RelativeSizeAxes = Axes.Both, + Colour = colours.Gray4, + }, + new UpdateableAvatar(User) + { + RelativeSizeAxes = Axes.Both, + }, + } + }, + usernameText = new OsuSpriteText + { + RelativeSizeAxes = Axes.X, + Width = 0.6f, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Colour = Color4.White, + Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), + Text = User.Username, + Truncate = true, + Shadow = false, + } + } + }, + } + }, + new Container + { + Padding = new MarginPadding { Top = 2f, Right = 17.5f, Bottom = 5f }, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Colour = Color4.White, + Children = new Drawable[] + { + scoreText = new OsuSpriteText + { + Spacing = new Vector2(-1f, 0f), + Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold, fixedWidth: true), + Shadow = false, + }, + accuracyText = new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold, fixedWidth: true), + Spacing = new Vector2(-1f, 0f), + Shadow = false, + }, + comboText = new OsuSpriteText + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Spacing = new Vector2(-1f, 0f), + Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold, fixedWidth: true), + Shadow = false, + }, + }, + } + } + } + } + }; + + TotalScore.BindValueChanged(v => scoreText.Text = v.NewValue.ToString("N0"), true); + Accuracy.BindValueChanged(v => accuracyText.Text = v.NewValue.FormatAccuracy(), true); + Combo.BindValueChanged(v => comboText.Text = $"{v.NewValue}x", true); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + updateColour(); + FinishTransforms(true); + } + + private const double panel_transition_duration = 500; + + private void updateColour() + { + if (scorePosition == 1) + { + mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, panel_transition_duration, Easing.OutElastic); + panelColour = Color4Extensions.FromHex("7fcc33"); + textColour = Color4.White; + } + else if (trackedPlayer) + { + mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, panel_transition_duration, Easing.OutElastic); + panelColour = Color4Extensions.FromHex("ffd966"); + textColour = Color4Extensions.FromHex("2e576b"); + } + else + { + mainFillContainer.ResizeWidthTo(regular_width, panel_transition_duration, Easing.OutElastic); + panelColour = Color4Extensions.FromHex("3399cc"); + textColour = Color4.White; + } + } + + private Color4 panelColour + { + set + { + mainFillContainer.FadeColour(value, panel_transition_duration, Easing.OutQuint); + centralFill.FadeColour(value, panel_transition_duration, Easing.OutQuint); + } + } + + private const double text_transition_duration = 200; + + private Color4 textColour + { + set + { + scoreText.FadeColour(value, text_transition_duration, Easing.OutQuint); + accuracyText.FadeColour(value, text_transition_duration, Easing.OutQuint); + comboText.FadeColour(value, text_transition_duration, Easing.OutQuint); + usernameText.FadeColour(value, text_transition_duration, Easing.OutQuint); + positionText.FadeColour(value, text_transition_duration, Easing.OutQuint); + } } } } diff --git a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs new file mode 100644 index 0000000000..bc1a03c5aa --- /dev/null +++ b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; + +namespace osu.Game.Screens.Play.HUD +{ + public interface ILeaderboardScore + { + BindableDouble TotalScore { get; } + BindableDouble Accuracy { get; } + BindableInt Combo { get; } + } +} diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs new file mode 100644 index 0000000000..12321de442 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -0,0 +1,131 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Configuration; +using osu.Game.Database; +using osu.Game.Online.API; +using osu.Game.Online.Spectator; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Screens.Play.HUD +{ + [LongRunningLoad] + public class MultiplayerGameplayLeaderboard : GameplayLeaderboard + { + private readonly ScoreProcessor scoreProcessor; + + private readonly int[] userIds; + + private readonly Dictionary userScores = new Dictionary(); + + /// + /// Construct a new leaderboard. + /// + /// A score processor instance to handle score calculation for scores of users in the match. + /// IDs of all users in this match. + public MultiplayerGameplayLeaderboard(ScoreProcessor scoreProcessor, int[] userIds) + { + // 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. + this.userIds = userIds; + } + + [Resolved] + private SpectatorStreamingClient streamingClient { get; set; } + + [Resolved] + private UserLookupCache userLookupCache { get; set; } + + private Bindable scoringMode; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config, IAPIProvider api) + { + streamingClient.OnNewFrames += handleIncomingFrames; + + foreach (var user in userIds) + { + streamingClient.WatchUser(user); + + // probably won't be required in the final implementation. + var resolvedUser = userLookupCache.GetUserAsync(user).Result; + + var trackedUser = new TrackedUserData(); + + userScores[user] = trackedUser; + var leaderboardScore = AddPlayer(resolvedUser, resolvedUser.Id == api.LocalUser.Value.Id); + + ((IBindable)leaderboardScore.Accuracy).BindTo(trackedUser.Accuracy); + ((IBindable)leaderboardScore.TotalScore).BindTo(trackedUser.Score); + ((IBindable)leaderboardScore.Combo).BindTo(trackedUser.CurrentCombo); + } + + scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode); + scoringMode.BindValueChanged(updateAllScores, true); + } + + private void updateAllScores(ValueChangedEvent mode) + { + foreach (var trackedData in userScores.Values) + trackedData.UpdateScore(scoreProcessor, mode.NewValue); + } + + private void handleIncomingFrames(int userId, FrameDataBundle bundle) + { + if (userScores.TryGetValue(userId, out var trackedData)) + { + trackedData.LastHeader = bundle.Header; + trackedData.UpdateScore(scoreProcessor, scoringMode.Value); + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (streamingClient != null) + { + foreach (var user in userIds) + { + streamingClient.StopWatchingUser(user); + } + + streamingClient.OnNewFrames -= handleIncomingFrames; + } + } + + private class TrackedUserData + { + public IBindableNumber Score => score; + + private readonly BindableDouble score = new BindableDouble(); + + public IBindableNumber Accuracy => accuracy; + + private readonly BindableDouble accuracy = new BindableDouble(1); + + public IBindableNumber CurrentCombo => currentCombo; + + private readonly BindableInt currentCombo = new BindableInt(); + + [CanBeNull] + public FrameHeader LastHeader; + + public void UpdateScore(ScoreProcessor processor, ScoringMode mode) + { + if (LastHeader == null) + return; + + (score.Value, accuracy.Value) = processor.GetScoreAndAccuracy(mode, LastHeader.MaxCombo, LastHeader.Statistics); + + currentCombo.Value = LastHeader.Combo; + } + } + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a54f9fc047..c539dff5d9 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -22,8 +23,10 @@ using osu.Game.Graphics.Containers; using osu.Game.IO.Archives; using osu.Game.Online.API; using osu.Game.Overlays; +using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Scoring; @@ -501,6 +504,7 @@ namespace osu.Game.Screens.Play } private ScheduledDelegate completionProgressDelegate; + private Task scoreSubmissionTask; private void updateCompletionState(ValueChangedEvent completionState) { @@ -527,33 +531,50 @@ namespace osu.Game.Screens.Play if (!showResults) return; - using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY)) - completionProgressDelegate = Schedule(GotoRanking); - } - - protected virtual ScoreInfo CreateScore() - { - var score = new ScoreInfo + scoreSubmissionTask ??= Task.Run(async () => { - Beatmap = Beatmap.Value.BeatmapInfo, - Ruleset = rulesetInfo, - Mods = Mods.Value.ToArray(), - }; + var score = CreateScore(); - if (DrawableRuleset.ReplayScore != null) - score.User = DrawableRuleset.ReplayScore.ScoreInfo?.User ?? new GuestUser(); - else - score.User = api.LocalUser.Value; + try + { + await SubmitScore(score); + } + catch (Exception ex) + { + Logger.Error(ex, "Score submission failed!"); + } - ScoreProcessor.PopulateScore(score); + try + { + await ImportScore(score); + } + catch (Exception ex) + { + Logger.Error(ex, "Score import failed!"); + } - return score; + return score.ScoreInfo; + }); + + using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY)) + scheduleCompletion(); } + private void scheduleCompletion() => completionProgressDelegate = Schedule(() => + { + if (!scoreSubmissionTask.IsCompleted) + { + scheduleCompletion(); + return; + } + + // screen may be in the exiting transition phase. + if (this.IsCurrentScreen()) + this.Push(CreateResults(scoreSubmissionTask.Result)); + }); + protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value; - protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, true); - #region Fail Logic protected FailOverlay FailOverlay { get; private set; } @@ -748,39 +769,74 @@ namespace osu.Game.Screens.Play return base.OnExiting(next); } - protected virtual void GotoRanking() + /// + /// Creates the player's . + /// + /// The . + protected virtual Score CreateScore() { + var score = new Score + { + ScoreInfo = new ScoreInfo + { + Beatmap = Beatmap.Value.BeatmapInfo, + Ruleset = rulesetInfo, + Mods = Mods.Value.ToArray(), + } + }; + if (DrawableRuleset.ReplayScore != null) { - // if a replay is present, we likely don't want to import into the local database. - this.Push(CreateResults(CreateScore())); - return; + score.ScoreInfo.User = DrawableRuleset.ReplayScore.ScoreInfo?.User ?? new GuestUser(); + score.Replay = DrawableRuleset.ReplayScore.Replay; } - - LegacyByteArrayReader replayReader = null; - - var score = new Score { ScoreInfo = CreateScore() }; - - if (recordingScore?.Replay.Frames.Count > 0) + else { - score.Replay = recordingScore.Replay; - - using (var stream = new MemoryStream()) - { - new LegacyScoreEncoder(score, gameplayBeatmap.PlayableBeatmap).Encode(stream); - replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); - } + score.ScoreInfo.User = api.LocalUser.Value; + score.Replay = new Replay { Frames = recordingScore?.Replay.Frames.ToList() ?? new List() }; } - scoreManager.Import(score.ScoreInfo, replayReader) - .ContinueWith(imported => Schedule(() => - { - // screen may be in the exiting transition phase. - if (this.IsCurrentScreen()) - this.Push(CreateResults(imported.Result)); - })); + ScoreProcessor.PopulateScore(score.ScoreInfo); + + return score; } + /// + /// Imports the player's to the local database. + /// + /// The to import. + /// The imported score. + protected virtual Task ImportScore(Score score) + { + // Replays are already populated and present in the game's database, so should not be re-imported. + if (DrawableRuleset.ReplayScore != null) + return Task.CompletedTask; + + LegacyByteArrayReader replayReader; + + using (var stream = new MemoryStream()) + { + new LegacyScoreEncoder(score, gameplayBeatmap.PlayableBeatmap).Encode(stream); + replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); + } + + return scoreManager.Import(score.ScoreInfo, replayReader); + } + + /// + /// Submits the player's . + /// + /// The to submit. + /// The submitted score. + protected virtual Task SubmitScore(Score score) => Task.CompletedTask; + + /// + /// Creates the for a . + /// + /// The to be displayed in the results screen. + /// The . + protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, true); + private void fadeOut(bool instant = false) { float fadeOutDuration = instant ? 0 : 250; diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 294d116f51..a07213cb33 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Threading.Tasks; using osu.Framework.Input.Bindings; using osu.Game.Input.Bindings; using osu.Game.Scoring; @@ -26,18 +27,21 @@ namespace osu.Game.Screens.Play DrawableRuleset?.SetReplayScore(Score); } - protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false); - - protected override ScoreInfo CreateScore() + protected override Score CreateScore() { var baseScore = base.CreateScore(); // Since the replay score doesn't contain statistics, we'll pass them through here. - Score.ScoreInfo.HitEvents = baseScore.HitEvents; + Score.ScoreInfo.HitEvents = baseScore.ScoreInfo.HitEvents; - return Score.ScoreInfo; + return Score; } + // Don't re-import replay scores as they're already present in the database. + protected override Task ImportScore(Score score) => Task.CompletedTask; + + protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false); + public bool OnPressed(GlobalAction action) { switch (action) diff --git a/osu.Game/Utils/FormatUtils.cs b/osu.Game/Utils/FormatUtils.cs index f2ab99f4b7..2578d8d835 100644 --- a/osu.Game/Utils/FormatUtils.cs +++ b/osu.Game/Utils/FormatUtils.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using Humanizer; + namespace osu.Game.Utils { public static class FormatUtils @@ -18,5 +20,11 @@ namespace osu.Game.Utils /// The accuracy to be formatted /// formatted accuracy in percentage public static string FormatAccuracy(this decimal accuracy) => $"{accuracy:0.00}%"; + + /// + /// Formats the supplied rank/leaderboard position in a consistent, simplified way. + /// + /// The rank/position to be formatted. + public static string FormatRank(this int rank) => rank.ToMetric(decimals: rank < 100_000 ? 1 : 0); } }