Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement support for viewing multiple profiles at once #247

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
18 changes: 17 additions & 1 deletion PerformanceCalculatorGUI/Components/ExtendedProfileScore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Utils;
using osu.Game.Users.Drawables;
using osuTK;
using PerformanceCalculatorGUI.Components.TextBoxes;

Expand All @@ -40,7 +41,7 @@

public PerformanceAttributes PerformanceAttributes { get; }

public ExtendedScore(SoloScoreInfo score, double? livePP, PerformanceAttributes attributes)

Check notice on line 44 in PerformanceCalculatorGUI/Components/ExtendedProfileScore.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Convert into primary constructor in PerformanceCalculatorGUI\Components\ExtendedProfileScore.cs on line 44
{
SoloScore = score;
PerformanceAttributes = attributes;
Expand Down Expand Up @@ -82,6 +83,8 @@

public readonly ExtendedScore Score;

public readonly bool ShowAvatar;

[Resolved]
private OsuColour colours { get; set; }

Expand All @@ -90,9 +93,10 @@

private OsuSpriteText positionChangeText;

public ExtendedProfileScore(ExtendedScore score)
public ExtendedProfileScore(ExtendedScore score, bool showAvatar = false)
{
Score = score;
ShowAvatar = showAvatar;

RelativeSizeAxes = Axes.X;
Height = height;
Expand All @@ -101,8 +105,20 @@
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
{
if (ShowAvatar)
{
AddInternal(new UpdateableAvatar(Score.SoloScore.User, false)
{
Size = new Vector2(height)
});
}

AddInternal(new ExtendedProfileItemContainer
{
// Resize to make room for avatar if necessary
X = ShowAvatar ? height : 0,
Padding = ShowAvatar ? new MarginPadding { Right = height } : new MarginPadding(),

OnHoverAction = () =>
{
positionChangeText.Text = $"#{Score.Position.Value}";
Expand Down
179 changes: 117 additions & 62 deletions PerformanceCalculatorGUI/Screens/ProfileScreen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@

private StatefulButton calculationButton;
private SwitchButton includePinnedCheckbox;
private SwitchButton onlyDisplayBestCheckbox;
private VerboseLoadingLayer loadingLayer;

private GridContainer layout;
Expand All @@ -48,7 +49,7 @@
private Container userPanelContainer;
private UserCard userPanel;

private string currentUser;
private string[] currentUsers = Array.Empty<string>();

private CancellationTokenSource calculationCancellatonToken;

Expand Down Expand Up @@ -121,8 +122,8 @@
{
RelativeSizeAxes = Axes.X,
Anchor = Anchor.TopLeft,
Label = "Username",
PlaceholderText = "peppy",
Label = "Username(s)",
PlaceholderText = "peppy, rloseise, peppy2",
CommitOnFocusLoss = false
},
calculationButton = new StatefulButton("Start calculation")
Expand Down Expand Up @@ -172,6 +173,20 @@
Font = OsuFont.Torus.With(weight: FontWeight.SemiBold, size: 14),
UseFullGlyphHeight = false,
Text = "Include pinned scores"
},
onlyDisplayBestCheckbox = new SwitchButton
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Current = { Value = true },
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.Torus.With(weight: FontWeight.SemiBold, size: 14),
UseFullGlyphHeight = false,
Text = "Only display best score on each beatmap"
}
}
},
Expand Down Expand Up @@ -209,20 +224,26 @@

usernameTextBox.OnCommit += (_, _) => { calculateProfile(usernameTextBox.Current.Value); };
sorting.ValueChanged += e => { updateSorting(e.NewValue); };
includePinnedCheckbox.Current.ValueChanged += e => { calculateProfile(currentUser); };
includePinnedCheckbox.Current.ValueChanged += e => { calculateProfile( string.Join(", ", currentUsers) ); };

Check failure on line 227 in PerformanceCalculatorGUI/Screens/ProfileScreen.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Space is redundant around parenthesis in PerformanceCalculatorGUI\Screens\ProfileScreen.cs on line 227

Check failure on line 227 in PerformanceCalculatorGUI/Screens/ProfileScreen.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Space is redundant around parenthesis in PerformanceCalculatorGUI\Screens\ProfileScreen.cs on line 227
onlyDisplayBestCheckbox.Current.ValueChanged += e => { calculateProfile( string.Join(", ", currentUsers) ); };

Check failure on line 228 in PerformanceCalculatorGUI/Screens/ProfileScreen.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Space is redundant around parenthesis in PerformanceCalculatorGUI\Screens\ProfileScreen.cs on line 228

Check failure on line 228 in PerformanceCalculatorGUI/Screens/ProfileScreen.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Space is redundant around parenthesis in PerformanceCalculatorGUI\Screens\ProfileScreen.cs on line 228

if (RuntimeInfo.IsDesktop)
HotReloadCallbackReceiver.CompilationFinished += _ => Schedule(() => { calculateProfile(currentUser); });
HotReloadCallbackReceiver.CompilationFinished += _ => Schedule(() => { calculateProfile( string.Join(",", currentUsers) ); });

Check failure on line 231 in PerformanceCalculatorGUI/Screens/ProfileScreen.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Space is redundant around parenthesis in PerformanceCalculatorGUI\Screens\ProfileScreen.cs on line 231

Check failure on line 231 in PerformanceCalculatorGUI/Screens/ProfileScreen.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Space is redundant around parenthesis in PerformanceCalculatorGUI\Screens\ProfileScreen.cs on line 231
}

private void calculateProfile(string username)
private void calculateProfile(string usernames)
{
if (string.IsNullOrEmpty(username))
if (string.IsNullOrEmpty(usernames))
{
usernameTextBox.FlashColour(Color4.Red, 1);
return;
}

Array.Clear(currentUsers);

string[] usernameArray = usernames.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest doing splitting only when committing from the textbox, and changing calculateProfile() to accept an array

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea, i've done that and also renamed the method calculateProfiles so it makes more sense. i can revert that if necessary

bool calculatingSingleProfile = usernameArray.Length <= 1;

calculationCancellatonToken?.Cancel();
calculationCancellatonToken?.Dispose();

Expand All @@ -236,22 +257,11 @@

Task.Run(async () =>
{
Schedule(() => loadingLayer.Text.Value = "Getting user data...");

var player = await apiManager.GetJsonFromApi<APIUser>($"users/{username}/{ruleset.Value.ShortName}");

currentUser = player.Username;

Schedule(() =>
{
if (userPanel != null)
userPanelContainer.Remove(userPanel, true);

userPanelContainer.Add(userPanel = new UserCard(player)
stanriders marked this conversation as resolved.
Show resolved Hide resolved
{
RelativeSizeAxes = Axes.X
});

sortingTabControl.Alpha = 1.0f;
sortingTabControl.Current.Value = ProfileSortCriteria.Local;

Expand All @@ -268,58 +278,98 @@
return;

var plays = new List<ExtendedScore>();

var players = new List<APIUser>();
var rulesetInstance = ruleset.Value.CreateInstance();

Schedule(() => loadingLayer.Text.Value = $"Calculating {player.Username} top scores...");
foreach (string username in usernameArray)
{
Schedule(() => loadingLayer.Text.Value = $"Getting {username} user data...");

var apiScores = await apiManager.GetJsonFromApi<List<SoloScoreInfo>>($"users/{player.OnlineID}/scores/best?mode={ruleset.Value.ShortName}&limit=100");
var player = await apiManager.GetJsonFromApi<APIUser>($"users/{username}/{ruleset.Value.ShortName}");
players.Add(player);
currentUsers = currentUsers.Append(player.Username).ToArray();

if (includePinnedCheckbox.Current.Value)
{
var pinnedScores = await apiManager.GetJsonFromApi<List<SoloScoreInfo>>($"users/{player.OnlineID}/scores/pinned?mode={ruleset.Value.ShortName}&limit=100");
apiScores = apiScores.Concat(pinnedScores.Where(p => !apiScores.Any(b => b.ID == p.ID))).ToList();
}
// Add user card if only calculating single profile
if (calculatingSingleProfile)
{
Schedule(() =>
{
userPanelContainer.Add(userPanel = new UserCard(player)
{
RelativeSizeAxes = Axes.X
});
});
}

foreach (var score in apiScores)
{
if (token.IsCancellationRequested)
return;
Schedule(() => loadingLayer.Text.Value = $"Calculating {player.Username} top scores...");

var working = ProcessorWorkingBeatmap.FromFileOrId(score.BeatmapID.ToString(), cachePath: configManager.GetBindable<string>(Settings.CachePath).Value);
var apiScores = await apiManager.GetJsonFromApi<List<SoloScoreInfo>>($"users/{player.OnlineID}/scores/best?mode={ruleset.Value.ShortName}&limit=100");

if (includePinnedCheckbox.Current.Value)
{
var pinnedScores = await apiManager.GetJsonFromApi<List<SoloScoreInfo>>($"users/{player.OnlineID}/scores/pinned?mode={ruleset.Value.ShortName}&limit=100");
apiScores = apiScores.Concat(pinnedScores.Where(p => !apiScores.Any(b => b.ID == p.ID))).ToList();

Check failure on line 311 in PerformanceCalculatorGUI/Screens/ProfileScreen.cs

View workflow job for this annotation

GitHub Actions / Code Quality

Captured variable is modified in the outer scope in PerformanceCalculatorGUI\Screens\ProfileScreen.cs on line 311
}

foreach (var score in apiScores)
{
if (token.IsCancellationRequested)
return;

Schedule(() => loadingLayer.Text.Value = $"Calculating {working.Metadata}");
var working = ProcessorWorkingBeatmap.FromFileOrId(score.BeatmapID.ToString(), cachePath: configManager.GetBindable<string>(Settings.CachePath).Value);

Mod[] mods = score.Mods.Select(x => x.ToMod(rulesetInstance)).ToArray();
Schedule(() => loadingLayer.Text.Value = $"Calculating {working.Metadata}");

var scoreInfo = score.ToScoreInfo(rulesets, working.BeatmapInfo);
Mod[] mods = score.Mods.Select(x => x.ToMod(rulesetInstance)).ToArray();

var parsedScore = new ProcessorScoreDecoder(working).Parse(scoreInfo);
var scoreInfo = score.ToScoreInfo(rulesets, working.BeatmapInfo);

var difficultyCalculator = rulesetInstance.CreateDifficultyCalculator(working);
var difficultyAttributes = difficultyCalculator.Calculate(mods);
var performanceCalculator = rulesetInstance.CreatePerformanceCalculator();
var parsedScore = new ProcessorScoreDecoder(working).Parse(scoreInfo);

double? livePp = score.PP;
var perfAttributes = await performanceCalculator?.CalculateAsync(parsedScore.ScoreInfo, difficultyAttributes, token)!;
score.PP = perfAttributes?.Total ?? 0.0;
var difficultyCalculator = rulesetInstance.CreateDifficultyCalculator(working);
var difficultyAttributes = difficultyCalculator.Calculate(mods);
var performanceCalculator = rulesetInstance.CreatePerformanceCalculator();

var extendedScore = new ExtendedScore(score, livePp, perfAttributes);
plays.Add(extendedScore);
double? livePp = score.PP;
var perfAttributes = await performanceCalculator?.CalculateAsync(parsedScore.ScoreInfo, difficultyAttributes, token)!;
score.PP = perfAttributes?.Total ?? 0.0;

Schedule(() => scores.Add(new ExtendedProfileScore(extendedScore)));
var extendedScore = new ExtendedScore(score, livePp, perfAttributes);
plays.Add(extendedScore);
}
}

if (token.IsCancellationRequested)
return;

// Filter plays if only displaying best score on each beatmap
if (onlyDisplayBestCheckbox.Current.Value)
{
Schedule(() => loadingLayer.Text.Value = "Filtering plays");

var filteredPlays = new List<ExtendedScore>();

// List of all beatmap IDs in plays without duplicates
var beatmapIDs = plays.Select(x => x.SoloScore.BeatmapID).Distinct().ToList();

foreach (int id in beatmapIDs)
{
var bestPlayOnBeatmap = plays.Where(x => x.SoloScore.BeatmapID == id).OrderByDescending(x => x.SoloScore.PP).First();
filteredPlays.Add(bestPlayOnBeatmap);
}

plays = filteredPlays;
}

var localOrdered = plays.OrderByDescending(x => x.SoloScore.PP).ToList();
var liveOrdered = plays.OrderByDescending(x => x.LivePP ?? 0).ToList();

Schedule(() =>
{
foreach (var play in plays)
{
scores.Add(new ExtendedProfileScore(play, !calculatingSingleProfile));

if (play.LivePP != null)
{
play.Position.Value = localOrdered.IndexOf(play) + 1;
Expand All @@ -328,29 +378,34 @@
}
});

decimal totalLocalPP = 0;
for (var i = 0; i < localOrdered.Count; i++)
totalLocalPP += (decimal)(Math.Pow(0.95, i) * (localOrdered[i].SoloScore.PP ?? 0));
if (calculatingSingleProfile)
{
var player = players.First();

decimal totalLivePP = player.Statistics.PP ?? (decimal)0.0;
decimal totalLocalPP = 0;
for (var i = 0; i < localOrdered.Count; i++)
totalLocalPP += (decimal)(Math.Pow(0.95, i) * (localOrdered[i].SoloScore.PP ?? 0));

decimal nonBonusLivePP = 0;
for (var i = 0; i < liveOrdered.Count; i++)
nonBonusLivePP += (decimal)(Math.Pow(0.95, i) * liveOrdered[i].LivePP ?? 0);
decimal totalLivePP = player.Statistics.PP ?? (decimal)0.0;

//todo: implement properly. this is pretty damn wrong.
var playcountBonusPP = (totalLivePP - nonBonusLivePP);
totalLocalPP += playcountBonusPP;
decimal nonBonusLivePP = 0;
for (var i = 0; i < liveOrdered.Count; i++)
nonBonusLivePP += (decimal)(Math.Pow(0.95, i) * liveOrdered[i].LivePP ?? 0);

Schedule(() =>
{
userPanel.Data.Value = new UserCardData
//todo: implement properly. this is pretty damn wrong.
var playcountBonusPP = (totalLivePP - nonBonusLivePP);
totalLocalPP += playcountBonusPP;

Schedule(() =>
{
LivePP = totalLivePP,
LocalPP = totalLocalPP,
PlaycountPP = playcountBonusPP
};
});
userPanel.Data.Value = new UserCardData
{
LivePP = totalLivePP,
LocalPP = totalLocalPP,
PlaycountPP = playcountBonusPP
};
});
}
}, token).ContinueWith(t =>
{
Logger.Log(t.Exception?.ToString(), level: LogLevel.Error);
Expand Down
Loading