1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 16:12:57 +08:00

Merge branch 'master' into abstract-room-manager

This commit is contained in:
smoogipoo 2020-12-20 17:35:01 +09:00
commit 3a4878558f
16 changed files with 821 additions and 185 deletions

View File

@ -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);
}
/// <summary>
@ -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;
}
}
}

View File

@ -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)

View File

@ -0,0 +1,155 @@
// 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 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<Drawable> 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<int> PlayingUsers => (BindableList<int>)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<int, FrameHeader> lastHeaders = new Dictionary<int, FrameHeader>();
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, int>
{
[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<LegacyReplayFrame>()));
}
}
protected override Task Connect() => Task.CompletedTask;
}
}
}

View File

@ -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<int> PlayingUsers => (BindableList<int>)base.PlayingUsers;

View File

@ -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<User> 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],
});
}
}
}

View File

@ -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()
};
}

View File

@ -68,7 +68,12 @@ namespace osu.Game.Rulesets.Scoring
private readonly double comboPortion;
private int maxAchievableCombo;
/// <summary>
/// The maximum achievable base score.
/// </summary>
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
}
}
/// <summary>
/// Given a minimal set of inputs, return the computed score and accuracy for the tracked beatmap / mods combination.
/// </summary>
/// <param name="mode">The <see cref="ScoringMode"/> to compute the total score in.</param>
/// <param name="maxCombo">The maximum combo achievable in the beatmap.</param>
/// <param name="statistics">Statistics to be used for calculating accuracy, bonus score, etc.</param>
/// <returns>The computed score and accuracy for provided inputs.</returns>
public (double score, double accuracy) GetScoreAndAccuracy(ScoringMode mode, int maxCombo, Dictionary<HitResult, int> 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<HitResult, int> statistics)
=> statistics.GetOrDefault(HitResult.SmallBonus) * Judgement.SMALL_BONUS_SCORE
+ statistics.GetOrDefault(HitResult.LargeBonus) * Judgement.LARGE_BONUS_SCORE;

View File

@ -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),

View File

@ -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<bool>();
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)

View File

@ -1,9 +1,8 @@
// 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 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
/// <summary>
/// Adds a player to the leaderboard.
/// </summary>
/// <param name="currentScore">The bindable current score of the player.</param>
/// <param name="user">The player.</param>
public void AddPlayer([NotNull] BindableDouble currentScore, [NotNull] User user)
/// <param name="isTracked">
/// Whether the player should be tracked on the leaderboard.
/// Set to <c>true</c> for the local player or a player whose replay is currently being played.
/// </param>
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++)
{

View File

@ -1,25 +1,39 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using 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;
/// <summary>
/// Creates a new <see cref="GameplayLeaderboardScore"/>.
/// </summary>
/// <param name="user">The score's player.</param>
/// <param name="trackedPlayer">Whether the player is the local user or a replay player.</param>
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);
}
}
}
}

View File

@ -0,0 +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 osu.Framework.Bindables;
namespace osu.Game.Screens.Play.HUD
{
public interface ILeaderboardScore
{
BindableDouble TotalScore { get; }
BindableDouble Accuracy { get; }
BindableInt Combo { get; }
}
}

View File

@ -0,0 +1,131 @@
// 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 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<int, TrackedUserData> userScores = new Dictionary<int, TrackedUserData>();
/// <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)
{
// 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> 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<double>)leaderboardScore.Accuracy).BindTo(trackedUser.Accuracy);
((IBindable<double>)leaderboardScore.TotalScore).BindTo(trackedUser.Score);
((IBindable<int>)leaderboardScore.Combo).BindTo(trackedUser.CurrentCombo);
}
scoringMode = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode);
scoringMode.BindValueChanged(updateAllScores, true);
}
private void updateAllScores(ValueChangedEvent<ScoringMode> 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<double> Score => score;
private readonly BindableDouble score = new BindableDouble();
public IBindableNumber<double> Accuracy => accuracy;
private readonly BindableDouble accuracy = new BindableDouble(1);
public IBindableNumber<int> 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;
}
}
}
}

View File

@ -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<ScoreInfo> scoreSubmissionTask;
private void updateCompletionState(ValueChangedEvent<bool> 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()
/// <summary>
/// Creates the player's <see cref="Score"/>.
/// </summary>
/// <returns>The <see cref="Score"/>.</returns>
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<ReplayFrame>() };
}
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;
}
/// <summary>
/// Imports the player's <see cref="Score"/> to the local database.
/// </summary>
/// <param name="score">The <see cref="Score"/> to import.</param>
/// <returns>The imported score.</returns>
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);
}
/// <summary>
/// Submits the player's <see cref="Score"/>.
/// </summary>
/// <param name="score">The <see cref="Score"/> to submit.</param>
/// <returns>The submitted score.</returns>
protected virtual Task SubmitScore(Score score) => Task.CompletedTask;
/// <summary>
/// Creates the <see cref="ResultsScreen"/> for a <see cref="ScoreInfo"/>.
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to be displayed in the results screen.</param>
/// <returns>The <see cref="ResultsScreen"/>.</returns>
protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, true);
private void fadeOut(bool instant = false)
{
float fadeOutDuration = instant ? 0 : 250;

View File

@ -1,6 +1,7 @@
// 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.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)

View File

@ -1,6 +1,8 @@
// 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;
namespace osu.Game.Utils
{
public static class FormatUtils
@ -18,5 +20,11 @@ namespace osu.Game.Utils
/// <param name="accuracy">The accuracy to be formatted</param>
/// <returns>formatted accuracy in percentage</returns>
public static string FormatAccuracy(this decimal accuracy) => $"{accuracy:0.00}%";
/// <summary>
/// Formats the supplied rank/leaderboard position in a consistent, simplified way.
/// </summary>
/// <param name="rank">The rank/position to be formatted.</param>
public static string FormatRank(this int rank) => rank.ToMetric(decimals: rank < 100_000 ? 1 : 0);
}
}