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
182 changes: 118 additions & 64 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,15 +122,15 @@
{
RelativeSizeAxes = Axes.X,
Anchor = Anchor.TopLeft,
Label = "Username",
PlaceholderText = "peppy",
Label = "Username(s)",
PlaceholderText = "peppy, rloseise, peppy2",
CommitOnFocusLoss = false
},
calculationButton = new StatefulButton("Start calculation")
{
Width = 150,
Height = username_container_height,
Action = () => { calculateProfile(usernameTextBox.Current.Value); }
Action = () => { calculateProfiles(usernameTextBox.Current.Value.Split(", ", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)); }
}
}
}
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 @@ -207,22 +222,27 @@
}
};

usernameTextBox.OnCommit += (_, _) => { calculateProfile(usernameTextBox.Current.Value); };
usernameTextBox.OnCommit += (_, _) => { calculateProfiles(usernameTextBox.Current.Value.Split(", ", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)); };
sorting.ValueChanged += e => { updateSorting(e.NewValue); };
includePinnedCheckbox.Current.ValueChanged += e => { calculateProfile(currentUser); };
includePinnedCheckbox.Current.ValueChanged += e => { calculateProfiles(currentUsers); };
onlyDisplayBestCheckbox.Current.ValueChanged += e => { calculateProfiles(currentUsers); };

if (RuntimeInfo.IsDesktop)
HotReloadCallbackReceiver.CompilationFinished += _ => Schedule(() => { calculateProfile(currentUser); });
HotReloadCallbackReceiver.CompilationFinished += _ => Schedule(() => { calculateProfiles(currentUsers); });
}

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

currentUsers = [];

bool calculatingSingleProfile = usernames.Length <= 1;

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

Expand All @@ -236,22 +256,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 +277,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 usernames)
{
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 310 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 310
}

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 +377,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