1
0
mirror of https://github.com/ppy/osu.git synced 2026-06-01 01:49:54 +08:00
Files
osu-lazer/osu.Game/Screens/Select/PanelLocalRankDisplay.cs
T
Bartłomiej Dach c2b96eb0f2 Fix crash when fetching top local rank in song select (#37722)
Closes https://github.com/ppy/osu/issues/37715.

The user's database contains several scores in which `ScoreInfo.Ruleset`
is null. How this happened, I'm not sure, it's probably custom rulesets.

The proper way to handle this would be to mark `ScoreInfo.Ruleset` as
nullable and deal with the hundred files of fallout, and also the fact
that `ScoreInfo` is an overloaded mess of a model that is sometimes a
database model and sometimes a post-converted online structure with
things backfilled to fit and I'm just not wanting to waste a week here,
so I'm choosing to look away.

Sidebar: You can't just put a null-propagating operator in the previous
conditional too because analysers will scream that `Ruleset` can't
*possibly* be null! So this uses `RulesetInfo.Equals(RulesetInfo?)`
because that can sorta-kinda handle nulls.
2026-05-12 15:39:00 +09:00

112 lines
3.3 KiB
C#

// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Online.Leaderboards;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osuTK;
using Realms;
namespace osu.Game.Screens.Select
{
public partial class PanelLocalRankDisplay : CompositeDrawable
{
private BeatmapInfo? beatmap;
public BeatmapInfo? Beatmap
{
get => beatmap;
set
{
beatmap = value;
if (IsLoaded)
updateSubscription();
}
}
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
[Resolved]
private RealmAccess realm { get; set; } = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
private IDisposable? scoreSubscription;
private readonly UpdateableRank updateable;
public bool HasRank => updateable.Rank != null;
public PanelLocalRankDisplay(BeatmapInfo? beatmap = null)
{
AutoSizeAxes = Axes.Both;
InternalChild = updateable = new UpdateableRank(animate: false)
{
Size = new Vector2(40, 20),
Alpha = 0,
};
Beatmap = beatmap;
}
protected override void LoadComplete()
{
base.LoadComplete();
ruleset.BindValueChanged(_ => updateSubscription(), true);
}
private void updateSubscription()
{
scoreSubscription?.Dispose();
setRankFromScore(null);
if (beatmap == null)
return;
scoreSubscription = realm.RegisterForNotifications(r => r.All<ScoreInfo>().Where(s => s.BeatmapHash == beatmap.Hash && !s.DeletePending), localScoresChanged);
}
private void localScoresChanged(IRealmCollection<ScoreInfo> sender, ChangeSet? changes)
{
// This subscription may fire from changes to linked beatmaps, which we don't care about.
// It's currently not possible for a score to be modified after insertion, so we can safely ignore callbacks with only modifications.
if (changes?.HasCollectionChanges() == false)
return;
ScoreInfo? topScore = sender
// doing these post realm filter is most efficient.
.Where(s => s.UserID == api.LocalUser.Value.Id || s.UserID <= 1)
.Where(s => ruleset.Value.Equals(s.Ruleset))
.MaxBy(info => (info.TotalScore, -info.Date.UtcDateTime.Ticks));
setRankFromScore(topScore);
}
private void setRankFromScore(ScoreInfo? topScore)
{
updateable.Rank = topScore?.Rank;
updateable.Alpha = topScore != null ? 1 : 0;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
scoreSubscription?.Dispose();
}
}
}