1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-26 12:45:09 +08:00

Merge pull request #14603 from smoogipoo/score-ordering

Fix scores not being ordered correctly on leaderboards
This commit is contained in:
Dean Herbert 2021-09-07 19:18:37 +09:00 committed by GitHub
commit fa62c846c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 320 additions and 188 deletions

View File

@ -160,11 +160,14 @@ namespace osu.Game.Tests.Visual.Playlists
Ruleset = { Value = new OsuRuleset().RulesetInfo }
}));
});
AddUntilStep("wait for load", () => resultsScreen.ChildrenOfType<ScorePanelList>().FirstOrDefault()?.AllPanelsVisible == true);
}
private void waitForDisplay()
{
AddUntilStep("wait for request to complete", () => requestComplete);
AddUntilStep("wait for panels to be visible", () => resultsScreen.ChildrenOfType<ScorePanelList>().FirstOrDefault()?.AllPanelsVisible == true);
AddWaitStep("wait for display", 5);
}

View File

@ -99,7 +99,7 @@ namespace osu.Game.Tests.Visual.Ranking
TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
AddStep("click expanded panel", () =>
{
@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.Ranking
TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
AddStep("click expanded panel", () =>
{
@ -177,7 +177,7 @@ namespace osu.Game.Tests.Visual.Ranking
TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
ScorePanel expandedPanel = null;
ScorePanel contractedPanel = null;
@ -223,6 +223,7 @@ namespace osu.Game.Tests.Visual.Ranking
TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
AddAssert("download button is disabled", () => !screen.ChildrenOfType<DownloadButton>().Last().Enabled.Value);

View File

@ -159,6 +159,9 @@ namespace osu.Game.Tests.Visual.Ranking
var firstScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
var secondScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
firstScore.User.Username = "A";
secondScore.User.Username = "B";
createListStep(() => new ScorePanelList());
AddStep("add scores and select first", () =>
@ -168,6 +171,8 @@ namespace osu.Game.Tests.Visual.Ranking
list.SelectedScore.Value = firstScore;
});
AddUntilStep("wait for load", () => list.AllPanelsVisible);
assertScoreState(firstScore, true);
assertScoreState(secondScore, false);
@ -182,6 +187,22 @@ namespace osu.Game.Tests.Visual.Ranking
assertExpandedPanelCentred();
}
[Test]
public void TestAddScoreImmediately()
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo);
createListStep(() =>
{
var newList = new ScorePanelList { SelectedScore = { Value = score } };
newList.AddScore(score);
return newList;
});
assertScoreState(score, true);
assertExpandedPanelCentred();
}
private void createListStep(Func<ScorePanelList> creationFunc)
{
AddStep("create list", () => Child = list = creationFunc().With(d =>

View File

@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.SongSelect
dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory));
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory));
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory, Scheduler));
return dependencies;
}

View File

@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private BeatmapManager beatmapManager;
private ScoreManager scoreManager;
private readonly List<ScoreInfo> scores = new List<ScoreInfo>();
private readonly List<ScoreInfo> importedScores = new List<ScoreInfo>();
private BeatmapInfo beatmap;
[Cached]
@ -82,7 +82,7 @@ namespace osu.Game.Tests.Visual.UserInterface
dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory));
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory));
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory, Scheduler));
beatmap = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).Result.Beatmaps[0];
@ -100,11 +100,9 @@ namespace osu.Game.Tests.Visual.UserInterface
User = new User { Username = "TestUser" },
};
scores.Add(scoreManager.Import(score).Result);
importedScores.Add(scoreManager.Import(score).Result);
}
scores.Sort(Comparer<ScoreInfo>.Create((s1, s2) => s2.TotalScore.CompareTo(s1.TotalScore)));
return dependencies;
}
@ -134,9 +132,14 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestDeleteViaRightClick()
{
ScoreInfo scoreBeingDeleted = null;
AddStep("open menu for top score", () =>
{
InputManager.MoveMouseTo(leaderboard.ChildrenOfType<LeaderboardScore>().First());
var leaderboardScore = leaderboard.ChildrenOfType<LeaderboardScore>().First();
scoreBeingDeleted = leaderboardScore.Score;
InputManager.MoveMouseTo(leaderboardScore);
InputManager.Click(MouseButton.Right);
});
@ -158,14 +161,14 @@ namespace osu.Game.Tests.Visual.UserInterface
InputManager.Click(MouseButton.Left);
});
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scores[0].OnlineScoreID));
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scoreBeingDeleted.OnlineScoreID));
}
[Test]
public void TestDeleteViaDatabase()
{
AddStep("delete top score", () => scoreManager.Delete(scores[0]));
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scores[0].OnlineScoreID));
AddStep("delete top score", () => scoreManager.Delete(importedScores[0]));
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != importedScores[0].OnlineScoreID));
}
}
}

View File

@ -34,6 +34,8 @@ namespace osu.Game.Online.Leaderboards
{
public const float HEIGHT = 60;
public readonly ScoreInfo Score;
private const float corner_radius = 5;
private const float edge_margin = 5;
private const float background_alpha = 0.25f;
@ -41,7 +43,6 @@ namespace osu.Game.Online.Leaderboards
protected Container RankContainer { get; private set; }
private readonly ScoreInfo score;
private readonly int? rank;
private readonly bool allowHighlight;
@ -67,7 +68,8 @@ namespace osu.Game.Online.Leaderboards
public LeaderboardScore(ScoreInfo score, int? rank, bool allowHighlight = true)
{
this.score = score;
Score = score;
this.rank = rank;
this.allowHighlight = allowHighlight;
@ -78,9 +80,9 @@ namespace osu.Game.Online.Leaderboards
[BackgroundDependencyLoader]
private void load(IAPIProvider api, OsuColour colour, ScoreManager scoreManager)
{
var user = score.User;
var user = Score.User;
statisticsLabels = GetStatistics(score).Select(s => new ScoreComponentLabel(s)).ToList();
statisticsLabels = GetStatistics(Score).Select(s => new ScoreComponentLabel(s)).ToList();
ClickableAvatar innerAvatar;
@ -198,7 +200,7 @@ namespace osu.Game.Online.Leaderboards
{
TextColour = Color4.White,
GlowColour = Color4Extensions.FromHex(@"83ccfa"),
Current = scoreManager.GetBindableTotalScoreString(score),
Current = scoreManager.GetBindableTotalScoreString(Score),
Font = OsuFont.Numeric.With(size: 23),
},
RankContainer = new Container
@ -206,7 +208,7 @@ namespace osu.Game.Online.Leaderboards
Size = new Vector2(40f, 20f),
Children = new[]
{
scoreRank = new UpdateableRank(score.Rank)
scoreRank = new UpdateableRank(Score.Rank)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -223,7 +225,7 @@ namespace osu.Game.Online.Leaderboards
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(1),
ChildrenEnumerable = score.Mods.Select(mod => new ModIcon(mod) { Scale = new Vector2(0.375f) })
ChildrenEnumerable = Score.Mods.Select(mod => new ModIcon(mod) { Scale = new Vector2(0.375f) })
},
},
},
@ -389,14 +391,14 @@ namespace osu.Game.Online.Leaderboards
{
List<MenuItem> items = new List<MenuItem>();
if (score.Mods.Length > 0 && modsContainer.Any(s => s.IsHovered) && songSelect != null)
items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = score.Mods));
if (Score.Mods.Length > 0 && modsContainer.Any(s => s.IsHovered) && songSelect != null)
items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods));
if (score.Files?.Count > 0)
items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => scoreManager.Export(score)));
if (Score.Files?.Count > 0)
items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => scoreManager.Export(Score)));
if (score.ID != 0)
items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(score))));
if (Score.ID != 0)
items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(Score))));
return items.ToArray();
}

View File

@ -243,7 +243,7 @@ namespace osu.Game
dependencies.Cache(fileStore = new FileStore(contextFactory, Storage));
// ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup()
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Host, () => difficultyCache, LocalConfig));
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Scheduler, Host, () => difficultyCache, LocalConfig));
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, true));
// this should likely be moved to ArchiveModelManager when another case appears where it is necessary

View File

@ -7,6 +7,8 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osuTK;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Beatmaps;
using osu.Game.Online.API;
@ -14,6 +16,7 @@ using osu.Game.Online.API.Requests;
using osu.Framework.Bindables;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Users;
@ -42,34 +45,46 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
[Resolved]
private RulesetStore rulesets { get; set; }
[Resolved]
private ScoreManager scoreManager { get; set; }
private GetScoresRequest getScoresRequest;
private CancellationTokenSource loadCancellationSource;
protected APILegacyScores Scores
{
set => Schedule(() =>
{
loadCancellationSource?.Cancel();
loadCancellationSource = new CancellationTokenSource();
topScoresContainer.Clear();
scoreTable.ClearScores();
scoreTable.Hide();
if (value?.Scores.Any() != true)
{
scoreTable.ClearScores();
scoreTable.Hide();
return;
}
var scoreInfos = value.Scores.Select(s => s.CreateScoreInfo(rulesets)).ToList();
var topScore = scoreInfos.First();
scoreManager.OrderByTotalScoreAsync(value.Scores.Select(s => s.CreateScoreInfo(rulesets)).ToArray(), loadCancellationSource.Token)
.ContinueWith(ordered => Schedule(() =>
{
if (loadCancellationSource.IsCancellationRequested)
return;
scoreTable.DisplayScores(scoreInfos, topScore.Beatmap?.Status.GrantsPerformancePoints() == true);
scoreTable.Show();
var topScore = ordered.Result.First();
var userScore = value.UserScore;
var userScoreInfo = userScore?.Score.CreateScoreInfo(rulesets);
scoreTable.DisplayScores(ordered.Result, topScore.Beatmap?.Status.GrantsPerformancePoints() == true);
scoreTable.Show();
topScoresContainer.Add(new DrawableTopScore(topScore));
var userScore = value.UserScore;
var userScoreInfo = userScore?.Score.CreateScoreInfo(rulesets);
if (userScoreInfo != null && userScoreInfo.OnlineScoreID != topScore.OnlineScoreID)
topScoresContainer.Add(new DrawableTopScore(userScoreInfo, userScore.Position));
topScoresContainer.Add(new DrawableTopScore(topScore));
if (userScoreInfo != null && userScoreInfo.OnlineScoreID != topScore.OnlineScoreID)
topScoresContainer.Add(new DrawableTopScore(userScoreInfo, userScore.Position));
}), TaskContinuationOptions.OnlyOnRanToCompletion);
});
}

View File

@ -13,6 +13,7 @@ using Microsoft.EntityFrameworkCore;
using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
@ -36,6 +37,7 @@ namespace osu.Game.Scoring
private readonly RulesetStore rulesets;
private readonly Func<BeatmapManager> beatmaps;
private readonly Scheduler scheduler;
[CanBeNull]
private readonly Func<BeatmapDifficultyCache> difficulties;
@ -43,12 +45,13 @@ namespace osu.Game.Scoring
[CanBeNull]
private readonly OsuConfigManager configManager;
public ScoreManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, IAPIProvider api, IDatabaseContextFactory contextFactory, IIpcHost importHost = null,
Func<BeatmapDifficultyCache> difficulties = null, OsuConfigManager configManager = null)
public ScoreManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, IAPIProvider api, IDatabaseContextFactory contextFactory, Scheduler scheduler,
IIpcHost importHost = null, Func<BeatmapDifficultyCache> difficulties = null, OsuConfigManager configManager = null)
: base(storage, contextFactory, api, new ScoreStore(contextFactory, storage), importHost)
{
this.rulesets = rulesets;
this.beatmaps = beatmaps;
this.scheduler = scheduler;
this.difficulties = difficulties;
this.configManager = configManager;
}
@ -103,6 +106,32 @@ namespace osu.Game.Scoring
=> base.CheckLocalAvailability(model, items)
|| (model.OnlineScoreID != null && items.Any(i => i.OnlineScoreID == model.OnlineScoreID));
/// <summary>
/// Orders an array of <see cref="ScoreInfo"/>s by total score.
/// </summary>
/// <param name="scores">The array of <see cref="ScoreInfo"/>s to reorder.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to cancel the process.</param>
/// <returns>The given <paramref name="scores"/> ordered by decreasing total score.</returns>
public async Task<ScoreInfo[]> OrderByTotalScoreAsync(ScoreInfo[] scores, CancellationToken cancellationToken = default)
{
var difficultyCache = difficulties?.Invoke();
if (difficultyCache != null)
{
// Compute difficulties asynchronously first to prevent blocking via the GetTotalScore() call below.
foreach (var s in scores)
{
await difficultyCache.GetDifficultyAsync(s.Beatmap, s.Ruleset, s.Mods, cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
}
}
// We're calling .Result, but this should not be a blocking call due to the above GetDifficultyAsync() calls.
return scores.OrderByDescending(s => GetTotalScoreAsync(s, cancellationToken: cancellationToken).Result)
.ThenBy(s => s.OnlineScoreID)
.ToArray();
}
/// <summary>
/// Retrieves a bindable that represents the total score of a <see cref="ScoreInfo"/>.
/// </summary>
@ -111,9 +140,9 @@ namespace osu.Game.Scoring
/// </remarks>
/// <param name="score">The <see cref="ScoreInfo"/> to retrieve the bindable for.</param>
/// <returns>The bindable containing the total score.</returns>
public Bindable<long> GetBindableTotalScore(ScoreInfo score)
public Bindable<long> GetBindableTotalScore([NotNull] ScoreInfo score)
{
var bindable = new TotalScoreBindable(score, difficulties);
var bindable = new TotalScoreBindable(score, this);
configManager?.BindWith(OsuSetting.ScoreDisplayMode, bindable.ScoringMode);
return bindable;
}
@ -126,7 +155,83 @@ namespace osu.Game.Scoring
/// </remarks>
/// <param name="score">The <see cref="ScoreInfo"/> to retrieve the bindable for.</param>
/// <returns>The bindable containing the formatted total score string.</returns>
public Bindable<string> GetBindableTotalScoreString(ScoreInfo score) => new TotalScoreStringBindable(GetBindableTotalScore(score));
public Bindable<string> GetBindableTotalScoreString([NotNull] ScoreInfo score) => new TotalScoreStringBindable(GetBindableTotalScore(score));
/// <summary>
/// Retrieves the total score of a <see cref="ScoreInfo"/> in the given <see cref="ScoringMode"/>.
/// The score is returned in a callback that is run on the update thread.
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to calculate the total score of.</param>
/// <param name="callback">The callback to be invoked with the total score.</param>
/// <param name="mode">The <see cref="ScoringMode"/> to return the total score as.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to cancel the process.</param>
public void GetTotalScore([NotNull] ScoreInfo score, [NotNull] Action<long> callback, ScoringMode mode = ScoringMode.Standardised, CancellationToken cancellationToken = default)
{
GetTotalScoreAsync(score, mode, cancellationToken)
.ContinueWith(s => scheduler.Add(() => callback(s.Result)), TaskContinuationOptions.OnlyOnRanToCompletion);
}
/// <summary>
/// Retrieves the total score of a <see cref="ScoreInfo"/> in the given <see cref="ScoringMode"/>.
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to calculate the total score of.</param>
/// <param name="mode">The <see cref="ScoringMode"/> to return the total score as.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to cancel the process.</param>
/// <returns>The total score.</returns>
public async Task<long> GetTotalScoreAsync([NotNull] ScoreInfo score, ScoringMode mode = ScoringMode.Standardised, CancellationToken cancellationToken = default)
{
if (score.Beatmap == null)
return score.TotalScore;
int beatmapMaxCombo;
double accuracy = score.Accuracy;
if (score.IsLegacyScore)
{
if (score.RulesetID == 3)
{
// In osu!stable, a full-GREAT score has 100% accuracy in mania. Along with a full combo, the score becomes indistinguishable from a full-PERFECT score.
// To get around this, recalculate accuracy based on the hit statistics.
// Note: This cannot be applied universally to all legacy scores, as some rulesets (e.g. catch) group multiple judgements together.
double maxBaseScore = score.Statistics.Select(kvp => kvp.Value).Sum() * Judgement.ToNumericResult(HitResult.Perfect);
double baseScore = score.Statistics.Select(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value).Sum();
if (maxBaseScore > 0)
accuracy = baseScore / maxBaseScore;
}
// This score is guaranteed to be an osu!stable score.
// The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used.
if (score.Beatmap.MaxCombo != null)
beatmapMaxCombo = score.Beatmap.MaxCombo.Value;
else
{
if (score.Beatmap.ID == 0 || difficulties == null)
{
// We don't have enough information (max combo) to compute the score, so use the provided score.
return score.TotalScore;
}
// We can compute the max combo locally after the async beatmap difficulty computation.
var difficulty = await difficulties().GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false);
beatmapMaxCombo = difficulty.MaxCombo;
}
}
else
{
// This is guaranteed to be a non-legacy score.
// The combo must be determined through the score's statistics, as both the beatmap's max combo and the difficulty calculator will provide osu!stable combo values.
beatmapMaxCombo = Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r.AffectsCombo()).Select(r => score.Statistics.GetValueOrDefault(r)).Sum();
}
if (beatmapMaxCombo == 0)
return 0;
var ruleset = score.Ruleset.CreateInstance();
var scoreProcessor = ruleset.CreateScoreProcessor();
scoreProcessor.Mods.Value = score.Mods;
return (long)Math.Round(scoreProcessor.GetScore(mode, beatmapMaxCombo, accuracy, (double)score.MaxCombo / beatmapMaxCombo, score.Statistics));
}
/// <summary>
/// Provides the total score of a <see cref="ScoreInfo"/>. Responds to changes in the currently-selected <see cref="ScoringMode"/>.
@ -136,99 +241,29 @@ namespace osu.Game.Scoring
public readonly Bindable<ScoringMode> ScoringMode = new Bindable<ScoringMode>();
private readonly ScoreInfo score;
private readonly Func<BeatmapDifficultyCache> difficulties;
private readonly ScoreManager scoreManager;
private CancellationTokenSource difficultyCalculationCancellationSource;
/// <summary>
/// Creates a new <see cref="TotalScoreBindable"/>.
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to provide the total score of.</param>
/// <param name="difficulties">A function to retrieve the <see cref="BeatmapDifficultyCache"/>.</param>
public TotalScoreBindable(ScoreInfo score, Func<BeatmapDifficultyCache> difficulties)
/// <param name="scoreManager">The <see cref="ScoreManager"/>.</param>
public TotalScoreBindable(ScoreInfo score, ScoreManager scoreManager)
{
this.score = score;
this.difficulties = difficulties;
this.scoreManager = scoreManager;
ScoringMode.BindValueChanged(onScoringModeChanged, true);
}
private IBindable<StarDifficulty?> difficultyBindable;
private CancellationTokenSource difficultyCancellationSource;
private void onScoringModeChanged(ValueChangedEvent<ScoringMode> mode)
{
difficultyCancellationSource?.Cancel();
difficultyCancellationSource = null;
difficultyCalculationCancellationSource?.Cancel();
difficultyCalculationCancellationSource = new CancellationTokenSource();
if (score.Beatmap == null)
{
Value = score.TotalScore;
return;
}
int beatmapMaxCombo;
double accuracy = score.Accuracy;
if (score.IsLegacyScore)
{
if (score.RulesetID == 3)
{
// In osu!stable, a full-GREAT score has 100% accuracy in mania. Along with a full combo, the score becomes indistinguishable from a full-PERFECT score.
// To get around this, recalculate accuracy based on the hit statistics.
// Note: This cannot be applied universally to all legacy scores, as some rulesets (e.g. catch) group multiple judgements together.
double maxBaseScore = score.Statistics.Select(kvp => kvp.Value).Sum() * Judgement.ToNumericResult(HitResult.Perfect);
double baseScore = score.Statistics.Select(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value).Sum();
if (maxBaseScore > 0)
accuracy = baseScore / maxBaseScore;
}
// This score is guaranteed to be an osu!stable score.
// The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used.
if (score.Beatmap.MaxCombo == null)
{
if (score.Beatmap.ID == 0 || difficulties == null)
{
// We don't have enough information (max combo) to compute the score, so use the provided score.
Value = score.TotalScore;
return;
}
// We can compute the max combo locally after the async beatmap difficulty computation.
difficultyBindable = difficulties().GetBindableDifficulty(score.Beatmap, score.Ruleset, score.Mods, (difficultyCancellationSource = new CancellationTokenSource()).Token);
difficultyBindable.BindValueChanged(d =>
{
if (d.NewValue is StarDifficulty diff)
updateScore(diff.MaxCombo, accuracy);
}, true);
return;
}
beatmapMaxCombo = score.Beatmap.MaxCombo.Value;
}
else
{
// This is guaranteed to be a non-legacy score.
// The combo must be determined through the score's statistics, as both the beatmap's max combo and the difficulty calculator will provide osu!stable combo values.
beatmapMaxCombo = Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r.AffectsCombo()).Select(r => score.Statistics.GetValueOrDefault(r)).Sum();
}
updateScore(beatmapMaxCombo, accuracy);
}
private void updateScore(int beatmapMaxCombo, double accuracy)
{
if (beatmapMaxCombo == 0)
{
Value = 0;
return;
}
var ruleset = score.Ruleset.CreateInstance();
var scoreProcessor = ruleset.CreateScoreProcessor();
scoreProcessor.Mods.Value = score.Mods;
Value = (long)Math.Round(scoreProcessor.GetScore(ScoringMode.Value, beatmapMaxCombo, accuracy, (double)score.MaxCombo / beatmapMaxCombo, score.Statistics));
scoreManager.GetTotalScore(score, s => Value = s, mode.NewValue, difficultyCalculationCancellationSource.Token);
}
}

View File

@ -32,6 +32,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
[Resolved]
private IAPIProvider api { get; set; }
[Resolved]
private ScoreManager scoreManager { get; set; }
public PlaylistsResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem, bool allowRetry, bool allowWatchingReplay = true)
: base(score, allowRetry, allowWatchingReplay)
{
@ -166,23 +169,28 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
/// <param name="pivot">An optional pivot around which the scores were retrieved.</param>
private void performSuccessCallback([NotNull] Action<IEnumerable<ScoreInfo>> callback, [NotNull] List<MultiplayerScore> scores, [CanBeNull] MultiplayerScores pivot = null)
{
var scoreInfos = new List<ScoreInfo>(scores.Select(s => s.CreateScoreInfo(playlistItem)));
var scoreInfos = scores.Select(s => s.CreateScoreInfo(playlistItem)).ToArray();
// Select a score if we don't already have one selected.
// Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll).
if (SelectedScore.Value == null)
// Score panels calculate total score before displaying, which can take some time. In order to count that calculation as part of the loading spinner display duration,
// calculate the total scores locally before invoking the success callback.
scoreManager.OrderByTotalScoreAsync(scoreInfos).ContinueWith(_ => Schedule(() =>
{
Schedule(() =>
// Select a score if we don't already have one selected.
// Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll).
if (SelectedScore.Value == null)
{
// Prefer selecting the local user's score, or otherwise default to the first visible score.
SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.Id == api.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault();
});
}
Schedule(() =>
{
// Prefer selecting the local user's score, or otherwise default to the first visible score.
SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.Id == api.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault();
});
}
// Invoke callback to add the scores. Exclude the user's current score which was added previously.
callback.Invoke(scoreInfos.Where(s => s.OnlineScoreID != Score?.OnlineScoreID));
// Invoke callback to add the scores. Exclude the user's current score which was added previously.
callback.Invoke(scoreInfos.Where(s => s.OnlineScoreID != Score?.OnlineScoreID));
hideLoadingSpinners(pivot);
hideLoadingSpinners(pivot);
}));
}
private void hideLoadingSpinners([CanBeNull] MultiplayerScores pivot = null)

View File

@ -52,8 +52,7 @@ namespace osu.Game.Screens.Ranking
private Drawable bottomPanel;
private Container<ScorePanel> detachedPanelContainer;
private bool fetchedInitialScores;
private APIRequest nextPageRequest;
private bool lastFetchCompleted;
private readonly bool allowRetry;
private readonly bool allowWatchingReplay;
@ -191,8 +190,10 @@ namespace osu.Game.Screens.Ranking
{
base.Update();
if (fetchedInitialScores && nextPageRequest == null)
if (lastFetchCompleted)
{
APIRequest nextPageRequest = null;
if (ScorePanelList.IsScrolledToStart)
nextPageRequest = FetchNextPage(-1, fetchScoresCallback);
else if (ScorePanelList.IsScrolledToEnd)
@ -200,10 +201,7 @@ namespace osu.Game.Screens.Ranking
if (nextPageRequest != null)
{
// Scheduled after children to give the list a chance to update its scroll position and not potentially trigger a second request too early.
nextPageRequest.Success += () => ScheduleAfterChildren(() => nextPageRequest = null);
nextPageRequest.Failure += _ => ScheduleAfterChildren(() => nextPageRequest = null);
lastFetchCompleted = false;
api.Queue(nextPageRequest);
}
}
@ -229,7 +227,7 @@ namespace osu.Game.Screens.Ranking
foreach (var s in scores)
addScore(s);
fetchedInitialScores = true;
lastFetchCompleted = true;
});
public override void OnEntering(IScreen last)

View File

@ -5,10 +5,14 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Scoring;
using osuTK;
@ -36,12 +40,14 @@ namespace osu.Game.Screens.Ranking
/// <summary>
/// Whether this <see cref="ScorePanelList"/> can be scrolled and is currently scrolled to the start.
/// </summary>
public bool IsScrolledToStart => flow.Count > 0 && scroll.ScrollableExtent > 0 && scroll.Current <= scroll_endpoint_distance;
public bool IsScrolledToStart => flow.Count > 0 && AllPanelsVisible && scroll.ScrollableExtent > 0 && scroll.Current <= scroll_endpoint_distance;
/// <summary>
/// Whether this <see cref="ScorePanelList"/> can be scrolled and is currently scrolled to the end.
/// </summary>
public bool IsScrolledToEnd => flow.Count > 0 && scroll.ScrollableExtent > 0 && scroll.IsScrolledToEnd(scroll_endpoint_distance);
public bool IsScrolledToEnd => flow.Count > 0 && AllPanelsVisible && scroll.ScrollableExtent > 0 && scroll.IsScrolledToEnd(scroll_endpoint_distance);
public bool AllPanelsVisible => flow.All(p => p.IsPresent);
/// <summary>
/// The current scroll position.
@ -60,6 +66,13 @@ namespace osu.Game.Screens.Ranking
public readonly Bindable<ScoreInfo> SelectedScore = new Bindable<ScoreInfo>();
[Resolved]
private ScoreManager scoreManager { get; set; }
[Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; }
private readonly CancellationTokenSource loadCancellationSource = new CancellationTokenSource();
private readonly Flow flow;
private readonly Scroll scroll;
private ScorePanel expandedPanel;
@ -90,6 +103,9 @@ namespace osu.Game.Screens.Ranking
{
base.LoadComplete();
foreach (var d in flow)
displayScore(d);
SelectedScore.BindValueChanged(selectedScoreChanged, true);
}
@ -114,36 +130,56 @@ namespace osu.Game.Screens.Ranking
};
});
flow.Add(panel.CreateTrackingContainer().With(d =>
var trackingContainer = panel.CreateTrackingContainer().With(d =>
{
d.Anchor = Anchor.Centre;
d.Origin = Anchor.Centre;
}));
d.Hide();
});
flow.Add(trackingContainer);
if (IsLoaded)
{
if (SelectedScore.Value == score)
{
SelectedScore.TriggerChange();
}
else
{
// We want the scroll position to remain relative to the expanded panel. When a new panel is added after the expanded panel, nothing needs to be done.
// But when a panel is added before the expanded panel, we need to offset the scroll position by the width of the new panel.
if (expandedPanel != null && flow.GetPanelIndex(score) < flow.GetPanelIndex(expandedPanel.Score))
{
// A somewhat hacky property is used here because we need to:
// 1) Scroll after the scroll container's visible range is updated.
// 2) Scroll before the scroll container's scroll position is updated.
// Without this, we would have a 1-frame positioning error which looks very jarring.
scroll.InstantScrollTarget = (scroll.InstantScrollTarget ?? scroll.Target) + ScorePanel.CONTRACTED_WIDTH + panel_spacing;
}
}
}
displayScore(trackingContainer);
return panel;
}
private void displayScore(ScorePanelTrackingContainer trackingContainer)
{
if (!IsLoaded)
return;
var score = trackingContainer.Panel.Score;
// Calculating score can take a while in extreme scenarios, so only display scores after the process completes.
scoreManager.GetTotalScoreAsync(score)
.ContinueWith(totalScore => Schedule(() =>
{
flow.SetLayoutPosition(trackingContainer, totalScore.Result);
trackingContainer.Show();
if (SelectedScore.Value == score)
{
SelectedScore.TriggerChange();
}
else
{
// We want the scroll position to remain relative to the expanded panel. When a new panel is added after the expanded panel, nothing needs to be done.
// But when a panel is added before the expanded panel, we need to offset the scroll position by the width of the new panel.
if (expandedPanel != null && flow.GetPanelIndex(score) < flow.GetPanelIndex(expandedPanel.Score))
{
// A somewhat hacky property is used here because we need to:
// 1) Scroll after the scroll container's visible range is updated.
// 2) Scroll before the scroll container's scroll position is updated.
// Without this, we would have a 1-frame positioning error which looks very jarring.
scroll.InstantScrollTarget = (scroll.InstantScrollTarget ?? scroll.Target) + ScorePanel.CONTRACTED_WIDTH + panel_spacing;
}
}
}), TaskContinuationOptions.OnlyOnRanToCompletion);
}
/// <summary>
/// Brings a <see cref="ScoreInfo"/> to the centre of the screen and expands it.
/// </summary>
@ -267,6 +303,9 @@ namespace osu.Game.Screens.Ranking
protected override bool OnKeyDown(KeyDownEvent e)
{
if (expandedPanel == null)
return base.OnKeyDown(e);
var expandedPanelIndex = flow.GetPanelIndex(expandedPanel.Score);
switch (e.Key)
@ -285,6 +324,12 @@ namespace osu.Game.Screens.Ranking
return base.OnKeyDown(e);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
loadCancellationSource?.Cancel();
}
private class Flow : FillFlowContainer<ScorePanelTrackingContainer>
{
public override IEnumerable<Drawable> FlowingChildren => applySorting(AliveInternalChildren);
@ -292,24 +337,8 @@ namespace osu.Game.Screens.Ranking
public int GetPanelIndex(ScoreInfo score) => applySorting(Children).TakeWhile(s => s.Panel.Score != score).Count();
private IEnumerable<ScorePanelTrackingContainer> applySorting(IEnumerable<Drawable> drawables) => drawables.OfType<ScorePanelTrackingContainer>()
.OrderByDescending(s => s.Panel.Score.TotalScore)
.OrderByDescending(GetLayoutPosition)
.ThenBy(s => s.Panel.Score.OnlineScoreID);
protected override int Compare(Drawable x, Drawable y)
{
var tX = (ScorePanelTrackingContainer)x;
var tY = (ScorePanelTrackingContainer)y;
int result = tY.Panel.Score.TotalScore.CompareTo(tX.Panel.Score.TotalScore);
if (result != 0)
return result;
if (tX.Panel.Score.OnlineScoreID == null || tY.Panel.Score.OnlineScoreID == null)
return base.Compare(x, y);
return tX.Panel.Score.OnlineScoreID.Value.CompareTo(tY.Panel.Score.OnlineScoreID.Value);
}
}
private class Scroll : OsuScrollContainer

View File

@ -4,6 +4,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
@ -66,6 +68,9 @@ namespace osu.Game.Screens.Select.Leaderboards
[Resolved]
private ScoreManager scoreManager { get; set; }
[Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; }
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; }
@ -120,8 +125,13 @@ namespace osu.Game.Screens.Select.Leaderboards
protected override bool IsOnlineScope => Scope != BeatmapLeaderboardScope.Local;
private CancellationTokenSource loadCancellationSource;
protected override APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback)
{
loadCancellationSource?.Cancel();
loadCancellationSource = new CancellationTokenSource();
if (Beatmap == null)
{
PlaceholderState = PlaceholderState.NoneSelected;
@ -146,8 +156,8 @@ namespace osu.Game.Screens.Select.Leaderboards
scores = scores.Where(s => s.Mods.Any(m => selectedMods.Contains(m.Acronym)));
}
Scores = scores.OrderByDescending(s => s.TotalScore).ToArray();
PlaceholderState = Scores.Any() ? PlaceholderState.Successful : PlaceholderState.NoScores;
scoreManager.OrderByTotalScoreAsync(scores.ToArray(), loadCancellationSource.Token)
.ContinueWith(ordered => scoresCallback?.Invoke(ordered.Result), TaskContinuationOptions.OnlyOnRanToCompletion);
return null;
}
@ -182,8 +192,15 @@ namespace osu.Game.Screens.Select.Leaderboards
req.Success += r =>
{
scoresCallback?.Invoke(r.Scores.Select(s => s.CreateScoreInfo(rulesets)));
TopScore = r.UserScore?.CreateScoreInfo(rulesets);
scoreManager.OrderByTotalScoreAsync(r.Scores.Select(s => s.CreateScoreInfo(rulesets)).ToArray(), loadCancellationSource.Token)
.ContinueWith(ordered => Schedule(() =>
{
if (loadCancellationSource.IsCancellationRequested)
return;
scoresCallback?.Invoke(ordered.Result);
TopScore = r.UserScore?.CreateScoreInfo(rulesets);
}), TaskContinuationOptions.OnlyOnRanToCompletion);
};
return req;