diff --git a/PerformanceCalculator/Difficulty/ModsCommand.cs b/PerformanceCalculator/Difficulty/ModsCommand.cs
index f922958..fb3aef9 100644
--- a/PerformanceCalculator/Difficulty/ModsCommand.cs
+++ b/PerformanceCalculator/Difficulty/ModsCommand.cs
@@ -47,6 +47,7 @@ namespace PerformanceCalculator.Difficulty
mod.RequiresConfiguration,
mod.UserPlayable,
mod.ValidForMultiplayer,
+ mod.ValidForFreestyleAsRequiredMod,
mod.ValidForMultiplayerAsFreeMod,
mod.AlwaysValidForSubmission,
});
diff --git a/PerformanceCalculator/PerformanceCalculator.csproj b/PerformanceCalculator/PerformanceCalculator.csproj
index f4ff578..77bd764 100644
--- a/PerformanceCalculator/PerformanceCalculator.csproj
+++ b/PerformanceCalculator/PerformanceCalculator.csproj
@@ -8,10 +8,10 @@
-
-
-
-
-
+
+
+
+
+
diff --git a/PerformanceCalculator/Profile/ProfileCommand.cs b/PerformanceCalculator/Profile/ProfileCommand.cs
index c55f515..6a461da 100644
--- a/PerformanceCalculator/Profile/ProfileCommand.cs
+++ b/PerformanceCalculator/Profile/ProfileCommand.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Threading;
using Alba.CsConsoleFormat;
using JetBrains.Annotations;
using McMaster.Extensions.CommandLineUtils;
@@ -27,6 +28,9 @@ namespace PerformanceCalculator.Profile
[AllowedValues("0", "1", "2", "3")]
public int? Ruleset { get; }
+ private const int max_api_scores = 200;
+ private const int max_api_scores_in_one_query = 100;
+
public override void Execute()
{
var displayPlays = new List();
@@ -39,22 +43,28 @@ namespace PerformanceCalculator.Profile
Console.WriteLine("Getting user top scores...");
- foreach (var play in GetJsonFromApi>($"users/{userData.Id}/scores/best?mode={rulesetApiName}&limit=100"))
+ var apiScores = new List();
+
+ for (int i = 0; i < max_api_scores; i += max_api_scores_in_one_query)
+ {
+ apiScores.AddRange(GetJsonFromApi>($"users/{userData.Id}/scores/best?mode={rulesetApiName}&limit={max_api_scores_in_one_query}&offset={i}"));
+ Thread.Sleep(200);
+ }
+
+ foreach (var play in apiScores)
{
var working = ProcessorWorkingBeatmap.FromFileOrId(play.BeatmapID.ToString());
Mod[] mods = play.Mods.Select(x => x.ToMod(ruleset)).ToArray();
- var scoreInfo = play.ToScoreInfo(mods);
+ var scoreInfo = play.ToScoreInfo(mods, working.BeatmapInfo);
scoreInfo.Ruleset = ruleset.RulesetInfo;
- var score = new ProcessorScoreDecoder(working).Parse(scoreInfo);
-
var difficultyCalculator = ruleset.CreateDifficultyCalculator(working);
var difficultyAttributes = difficultyCalculator.Calculate(scoreInfo.Mods);
var performanceCalculator = ruleset.CreatePerformanceCalculator();
- var ppAttributes = performanceCalculator?.Calculate(score.ScoreInfo, difficultyAttributes);
+ var ppAttributes = performanceCalculator?.Calculate(scoreInfo, difficultyAttributes);
var thisPlay = new UserPlayInfo
{
Beatmap = working.BeatmapInfo,
diff --git a/PerformanceCalculator/Simulate/SimulateCommand.cs b/PerformanceCalculator/Simulate/SimulateCommand.cs
index 3274afe..af404b4 100644
--- a/PerformanceCalculator/Simulate/SimulateCommand.cs
+++ b/PerformanceCalculator/Simulate/SimulateCommand.cs
@@ -40,6 +40,10 @@ namespace PerformanceCalculator.Simulate
[Option(Template = "-X|--misses ", Description = "Number of misses. Defaults to 0.")]
public int Misses { get; }
+ [UsedImplicitly]
+ [Option(Template = "-l|--legacy-total-score ", Description = "Amount of legacy total score.")]
+ public long? LegacyTotalScore { get; }
+
//
// Options implemented in the ruleset-specific commands
// -> Catch renames Mehs/Goods to (tiny-)droplets
@@ -73,6 +77,7 @@ namespace PerformanceCalculator.Simulate
Accuracy = GetAccuracy(beatmap, statistics, mods),
MaxCombo = Combo ?? (int)Math.Round(PercentCombo / 100 * beatmapMaxCombo),
Statistics = statistics,
+ LegacyTotalScore = LegacyTotalScore,
Mods = mods
};
diff --git a/PerformanceCalculatorGUI/Components/BeatmapCard.cs b/PerformanceCalculatorGUI/Components/BeatmapCard.cs
index 73c88f1..9f01812 100644
--- a/PerformanceCalculatorGUI/Components/BeatmapCard.cs
+++ b/PerformanceCalculatorGUI/Components/BeatmapCard.cs
@@ -2,21 +2,26 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Events;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
+using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
+using osu.Game.Overlays.Mods;
+using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Utils;
using osuTK;
@@ -24,7 +29,7 @@ using PerformanceCalculatorGUI.Components.TextBoxes;
namespace PerformanceCalculatorGUI.Components
{
- public partial class BeatmapCard : OsuClickableContainer
+ public partial class BeatmapCard : OsuClickableContainer, IHasCustomTooltip
{
private readonly ProcessorWorkingBeatmap beatmap;
@@ -40,6 +45,10 @@ namespace PerformanceCalculatorGUI.Components
[Resolved]
private Bindable> mods { get; set; }
+ public ITooltip GetCustomTooltip() => new BeatmapCardTooltip(colourProvider);
+ public ProcessorWorkingBeatmap TooltipContent { get; }
+
+ private ModSettingChangeTracker modSettingChangeTracker;
private OsuSpriteText bpmText = null!;
public BeatmapCard(ProcessorWorkingBeatmap beatmap)
@@ -49,6 +58,7 @@ namespace PerformanceCalculatorGUI.Components
RelativeSizeAxes = Axes.X;
Height = 40;
CornerRadius = ExtendedLabelledTextBox.CORNER_RADIUS;
+ TooltipContent = beatmap;
}
[BackgroundDependencyLoader]
@@ -115,7 +125,13 @@ namespace PerformanceCalculatorGUI.Components
Action = () => { host.OpenUrlExternally($"https://osu.ppy.sh/beatmaps/{beatmap.BeatmapInfo.OnlineID}"); };
- mods.BindValueChanged(_ => updateBpm());
+ mods.BindValueChanged(_ =>
+ {
+ modSettingChangeTracker?.Dispose();
+ modSettingChangeTracker = new ModSettingChangeTracker(mods.Value);
+ modSettingChangeTracker.SettingChanged += _ => updateBpm();
+ updateBpm();
+ }, true);
updateBpm();
}
@@ -146,5 +162,146 @@ namespace PerformanceCalculatorGUI.Components
bpmText.Text = labelText;
}
+
+ public partial class BeatmapCardTooltip : VisibilityContainer, ITooltip
+ {
+ public BeatmapCardTooltip(OverlayColourProvider colourProvider)
+ {
+ this.colourProvider = colourProvider;
+ AutoSizeAxes = Axes.Both;
+ Masking = true;
+ CornerRadius = 8;
+ }
+
+ protected override void PopIn() => this.FadeIn(150, Easing.OutQuint);
+ protected override void PopOut() => this.Delay(150).FadeOut(500, Easing.OutQuint);
+
+ public void Move(Vector2 pos) => Position = pos;
+
+ private ProcessorWorkingBeatmap beatmap;
+
+ private VerticalAttributeDisplay keyCountDisplay = null!;
+ private VerticalAttributeDisplay circleSizeDisplay = null!;
+ private VerticalAttributeDisplay drainRateDisplay = null!;
+ private VerticalAttributeDisplay approachRateDisplay = null!;
+ private VerticalAttributeDisplay overallDifficultyDisplay = null!;
+
+ [Resolved]
+ private Bindable> mods { get; set; }
+
+ private readonly OverlayColourProvider colourProvider;
+
+ private ModSettingChangeTracker modSettingChangeTracker;
+
+ [Resolved]
+ private IBindable ruleset { get; set; }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ mods.BindValueChanged(_ =>
+ {
+ modSettingChangeTracker?.Dispose();
+ modSettingChangeTracker = new ModSettingChangeTracker(mods.Value);
+ modSettingChangeTracker.SettingChanged += _ => updateValues();
+ updateValues();
+ }, true);
+
+ ruleset.BindValueChanged(_ => updateValues());
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e) => true;
+
+ protected override bool OnClick(ClickEvent e) => true;
+
+ private void updateValues() => Scheduler.AddOnce(() =>
+ {
+ if (beatmap?.BeatmapInfo == null)
+ return;
+
+ double rate = ModUtils.CalculateRateWithMods(mods.Value);
+
+ BeatmapDifficulty originalDifficulty = new BeatmapDifficulty(beatmap.BeatmapInfo.Difficulty);
+ BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(originalDifficulty);
+
+ foreach (var mod in mods.Value.OfType())
+ mod.ApplyToDifficulty(adjustedDifficulty);
+
+ Ruleset rulesetInstance = ruleset.Value.CreateInstance();
+ adjustedDifficulty = rulesetInstance.GetRateAdjustedDisplayDifficulty(adjustedDifficulty, rate);
+
+ if (ruleset.Value.OnlineID >= 0)
+ {
+ if (ruleset.Value.ShortName is "osu" or "fruits")
+ {
+ circleSizeDisplay.Show();
+ circleSizeDisplay.AdjustType.Value = VerticalAttributeDisplay.CalculateEffect(originalDifficulty.CircleSize, adjustedDifficulty.CircleSize);
+ circleSizeDisplay.Current.Value = adjustedDifficulty.CircleSize;
+
+ approachRateDisplay.Show();
+ approachRateDisplay.AdjustType.Value = VerticalAttributeDisplay.CalculateEffect(originalDifficulty.ApproachRate, adjustedDifficulty.ApproachRate);
+ approachRateDisplay.Current.Value = adjustedDifficulty.ApproachRate;
+ }
+ else
+ {
+ circleSizeDisplay.Hide();
+ approachRateDisplay.Hide();
+ }
+
+ if (ruleset.Value.ShortName == "mania")
+ {
+ ILegacyRuleset legacyRuleset = (ILegacyRuleset)ruleset.Value.CreateInstance();
+ int keyCount = legacyRuleset.GetKeyCount(beatmap.BeatmapInfo, mods.Value);
+ int keyCountOriginal = legacyRuleset.GetKeyCount(beatmap.BeatmapInfo, []);
+
+ keyCountDisplay.Show();
+ keyCountDisplay.AdjustType.Value = VerticalAttributeDisplay.CalculateEffect(keyCountOriginal, keyCount);
+ keyCountDisplay.Current.Value = keyCount;
+ }
+ else
+ {
+ keyCountDisplay.Hide();
+ }
+ }
+
+ drainRateDisplay.AdjustType.Value = VerticalAttributeDisplay.CalculateEffect(originalDifficulty.DrainRate, adjustedDifficulty.DrainRate);
+ overallDifficultyDisplay.AdjustType.Value = VerticalAttributeDisplay.CalculateEffect(originalDifficulty.OverallDifficulty, adjustedDifficulty.OverallDifficulty);
+
+ drainRateDisplay.Current.Value = adjustedDifficulty.DrainRate;
+ overallDifficultyDisplay.Current.Value = adjustedDifficulty.OverallDifficulty;
+ });
+
+ public void SetContent(ProcessorWorkingBeatmap content)
+ {
+ if (content == beatmap && Children.Any())
+ return;
+
+ beatmap = content;
+
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colourProvider.Background6
+ },
+ new FillFlowContainer
+ {
+ Padding = new MarginPadding(8),
+ AutoSizeAxes = Axes.Both,
+ Direction = FillDirection.Horizontal,
+ Children = new Drawable[]
+ {
+ keyCountDisplay = new VerticalAttributeDisplay("Keys") { AutoSizeAxes = Axes.Both, Alpha = 0 },
+ circleSizeDisplay = new VerticalAttributeDisplay("CS") { AutoSizeAxes = Axes.Both, Alpha = 0 },
+ drainRateDisplay = new VerticalAttributeDisplay("HP") { AutoSizeAxes = Axes.Both },
+ overallDifficultyDisplay = new VerticalAttributeDisplay("OD") { AutoSizeAxes = Axes.Both },
+ approachRateDisplay = new VerticalAttributeDisplay("AR") { AutoSizeAxes = Axes.Both, Alpha = 0 },
+ }
+ }
+ };
+ }
+ }
}
}
diff --git a/PerformanceCalculatorGUI/Components/ExtendedProfileScore.cs b/PerformanceCalculatorGUI/Components/ExtendedProfileScore.cs
index 6f5d569..19db0e3 100644
--- a/PerformanceCalculatorGUI/Components/ExtendedProfileScore.cs
+++ b/PerformanceCalculatorGUI/Components/ExtendedProfileScore.cs
@@ -25,6 +25,7 @@ using osu.Game.Rulesets.Difficulty;
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;
@@ -74,6 +75,7 @@ namespace PerformanceCalculatorGUI.Components
public partial class ExtendedProfileScore : CompositeDrawable
{
private const int height = 35;
+ private const int avatar_size = 35;
private const int performance_width = 100;
private const int rank_difference_width = 35;
private const int small_text_font_size = 11;
@@ -82,6 +84,8 @@ namespace PerformanceCalculatorGUI.Components
public readonly ExtendedScore Score;
+ public readonly bool ShowAvatar;
+
[Resolved]
private OsuColour colours { get; set; }
@@ -90,17 +94,20 @@ namespace PerformanceCalculatorGUI.Components
private OsuSpriteText positionChangeText;
- public ExtendedProfileScore(ExtendedScore score)
+ public ExtendedProfileScore(ExtendedScore score, bool showAvatar = false)
{
Score = score;
+ ShowAvatar = showAvatar;
RelativeSizeAxes = Axes.X;
Height = height;
}
[BackgroundDependencyLoader]
- private void load(RulesetStore rulesets)
+ private void load(GameHost host, RulesetStore rulesets)
{
+ int avatarPadding = ShowAvatar ? avatar_size : 0;
+
AddInternal(new ExtendedProfileItemContainer
{
OnHoverAction = () =>
@@ -111,8 +118,17 @@ namespace PerformanceCalculatorGUI.Components
{
positionChangeText.Text = $"{Score.PositionChange.Value:+0;-0;-}";
},
- Children = new Drawable[]
+ Children = new[]
{
+ ShowAvatar
+ ? new ClickableAvatar(Score.SoloScore.User, true)
+ {
+ Masking = true,
+ CornerRadius = ExtendedLabelledTextBox.CORNER_RADIUS,
+ Size = new Vector2(avatar_size),
+ Action = () => { host.OpenUrlExternally($"https://osu.ppy.sh/users/{Score.SoloScore.User?.Id}"); }
+ }
+ : Empty(),
new Container
{
Name = "Rank difference",
@@ -120,6 +136,7 @@ namespace PerformanceCalculatorGUI.Components
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Width = rank_difference_width,
+ Margin = new MarginPadding { Left = avatarPadding },
Child = positionChangeText = new OsuSpriteText
{
Anchor = Anchor.Centre,
@@ -132,7 +149,7 @@ namespace PerformanceCalculatorGUI.Components
{
Name = "Score info",
RelativeSizeAxes = Axes.Both,
- Padding = new MarginPadding { Left = rank_difference_width, Right = performance_width },
+ Padding = new MarginPadding { Left = rank_difference_width + avatarPadding, Right = performance_width },
Children = new Drawable[]
{
new FillFlowContainer
diff --git a/PerformanceCalculatorGUI/Components/TextBoxes/ReadonlyOsuTextBox.cs b/PerformanceCalculatorGUI/Components/TextBoxes/ReadonlyOsuTextBox.cs
new file mode 100644
index 0000000..542f63b
--- /dev/null
+++ b/PerformanceCalculatorGUI/Components/TextBoxes/ReadonlyOsuTextBox.cs
@@ -0,0 +1,42 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Game.Graphics.UserInterface;
+
+namespace PerformanceCalculatorGUI.Components.TextBoxes
+{
+ public partial class ReadonlyOsuTextBox : OsuTextBox
+ {
+ private readonly string text;
+ private readonly bool hasBackground;
+
+ public ReadonlyOsuTextBox(string text, bool hasBackground = true)
+ {
+ this.text = text;
+ this.hasBackground = hasBackground;
+
+ Text = text;
+ }
+
+ protected override void LoadComplete()
+ {
+ if (!hasBackground)
+ BackgroundUnfocused = BackgroundUnfocused.Opacity(0);
+
+ base.LoadComplete();
+ }
+
+ protected override void OnUserTextAdded(string added)
+ {
+ NotifyInputError();
+ Text = text;
+ }
+
+ protected override void OnUserTextRemoved(string removed)
+ {
+ NotifyInputError();
+ Text = text;
+ }
+ }
+}
diff --git a/PerformanceCalculatorGUI/PerformanceCalculatorGUI.csproj b/PerformanceCalculatorGUI/PerformanceCalculatorGUI.csproj
index 5f4eca0..ce3cd18 100644
--- a/PerformanceCalculatorGUI/PerformanceCalculatorGUI.csproj
+++ b/PerformanceCalculatorGUI/PerformanceCalculatorGUI.csproj
@@ -6,10 +6,10 @@
latest
-
-
-
-
-
+
+
+
+
+
diff --git a/PerformanceCalculatorGUI/ProcessorScoreDecoder.cs b/PerformanceCalculatorGUI/ProcessorScoreDecoder.cs
index 0fc7902..84cb0af 100644
--- a/PerformanceCalculatorGUI/ProcessorScoreDecoder.cs
+++ b/PerformanceCalculatorGUI/ProcessorScoreDecoder.cs
@@ -24,7 +24,7 @@ namespace PerformanceCalculatorGUI
public Score Parse(ScoreInfo scoreInfo)
{
var score = new Score { ScoreInfo = scoreInfo };
- score.ScoreInfo.LegacyTotalScore = score.ScoreInfo.TotalScore;
+ score.ScoreInfo.LegacyTotalScore ??= score.ScoreInfo.TotalScore;
PopulateMaximumStatistics(score.ScoreInfo, beatmap);
StandardisedScoreMigrationTools.UpdateFromLegacy(score.ScoreInfo, beatmap);
return score;
diff --git a/PerformanceCalculatorGUI/Screens/ObjectInspection/ObjectInspector.cs b/PerformanceCalculatorGUI/Screens/ObjectInspection/ObjectInspector.cs
index dafd2d7..36d5eea 100644
--- a/PerformanceCalculatorGUI/Screens/ObjectInspection/ObjectInspector.cs
+++ b/PerformanceCalculatorGUI/Screens/ObjectInspection/ObjectInspector.cs
@@ -26,6 +26,7 @@ using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Edit.Components.Timelines.Summary;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
+using osu.Game.Skinning;
using osuTK.Input;
namespace PerformanceCalculatorGUI.Screens.ObjectInspection
@@ -133,7 +134,7 @@ namespace PerformanceCalculatorGUI.Screens.ObjectInspection
timeline = new Timeline(new TimelineBlueprintContainer())
}
},
- rulesetContainer = new Container
+ rulesetContainer = new RulesetSkinProvidingContainer(rulesetInstance, playableBeatmap, null)
{
Origin = Anchor.TopRight,
Anchor = Anchor.TopRight,
diff --git a/PerformanceCalculatorGUI/Screens/ProfileScreen.cs b/PerformanceCalculatorGUI/Screens/ProfileScreen.cs
index 79b9bf1..60e83d1 100644
--- a/PerformanceCalculatorGUI/Screens/ProfileScreen.cs
+++ b/PerformanceCalculatorGUI/Screens/ProfileScreen.cs
@@ -38,6 +38,7 @@ namespace PerformanceCalculatorGUI.Screens
private StatefulButton calculationButton;
private SwitchButton includePinnedCheckbox;
+ private SwitchButton onlyDisplayBestCheckbox;
private VerboseLoadingLayer loadingLayer;
private GridContainer layout;
@@ -48,7 +49,7 @@ namespace PerformanceCalculatorGUI.Screens
private Container userPanelContainer;
private UserCard userPanel;
- private string currentUser;
+ private string[] currentUsers = Array.Empty();
private CancellationTokenSource calculationCancellatonToken;
@@ -73,6 +74,8 @@ namespace PerformanceCalculatorGUI.Screens
public override bool ShouldShowConfirmationDialogOnSwitch => false;
private const float username_container_height = 40;
+ private const int max_api_scores = 200;
+ private const int max_api_scores_in_one_query = 100;
public ProfileScreen()
{
@@ -121,15 +124,15 @@ namespace PerformanceCalculatorGUI.Screens
{
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)); }
}
}
}
@@ -172,6 +175,20 @@ namespace PerformanceCalculatorGUI.Screens
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"
}
}
},
@@ -207,17 +224,20 @@ namespace PerformanceCalculatorGUI.Screens
}
};
- 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))
+ currentUsers = usernames.Distinct().ToArray();
+
+ if (usernames.Length < 1)
{
usernameTextBox.FlashColour(Color4.Red, 1);
return;
@@ -236,22 +256,11 @@ namespace PerformanceCalculatorGUI.Screens
Task.Run(async () =>
{
- Schedule(() => loadingLayer.Text.Value = "Getting user data...");
-
- var player = await apiManager.GetJsonFromApi($"users/{username}/{ruleset.Value.ShortName}").ConfigureAwait(false);
-
- currentUser = player.Username;
-
Schedule(() =>
{
if (userPanel != null)
userPanelContainer.Remove(userPanel, true);
- userPanelContainer.Add(userPanel = new UserCard(player)
- {
- RelativeSizeAxes = Axes.X
- });
-
sortingTabControl.Alpha = 1.0f;
sortingTabControl.Current.Value = ProfileSortCriteria.Local;
@@ -268,53 +277,107 @@ namespace PerformanceCalculatorGUI.Screens
return;
var plays = new List();
-
+ var players = new List();
var rulesetInstance = ruleset.Value.CreateInstance();
- Schedule(() => loadingLayer.Text.Value = $"Calculating {player.Username} top scores...");
-
- var apiScores = await apiManager.GetJsonFromApi>($"users/{player.OnlineID}/scores/best?mode={ruleset.Value.ShortName}&limit=100").ConfigureAwait(false);
-
- if (includePinnedCheckbox.Current.Value)
+ foreach (string username in currentUsers)
{
- var pinnedScores = await apiManager.GetJsonFromApi>($"users/{player.OnlineID}/scores/pinned?mode={ruleset.Value.ShortName}&limit=100").ConfigureAwait(false);
- apiScores = apiScores.Concat(pinnedScores.Where(p => !apiScores.Any(b => b.ID == p.ID)).ToArray()).ToList();
- }
+ try
+ {
+ Schedule(() => loadingLayer.Text.Value = $"Getting {username} user data...");
- foreach (var score in apiScores)
- {
- if (token.IsCancellationRequested)
- return;
+ var player = await apiManager.GetJsonFromApi($"users/{username}/{ruleset.Value.ShortName}").ConfigureAwait(false);
+ players.Add(player);
- var working = ProcessorWorkingBeatmap.FromFileOrId(score.BeatmapID.ToString(), cachePath: configManager.GetBindable(Settings.CachePath).Value);
+ Schedule(() => loadingLayer.Text.Value = $"Calculating {player.Username} top scores...");
- Schedule(() => loadingLayer.Text.Value = $"Calculating {working.Metadata}");
+ var apiScores = new List();
- Mod[] mods = score.Mods.Select(x => x.ToMod(rulesetInstance)).ToArray();
+ for (int i = 0; i < max_api_scores; i += max_api_scores_in_one_query)
+ {
+ apiScores.AddRange(await apiManager.GetJsonFromApi>($"users/{player.OnlineID}/scores/best?mode={ruleset.Value.ShortName}&limit={max_api_scores_in_one_query}&offset={i}").ConfigureAwait(false));
+ await Task.Delay(200, token).ConfigureAwait(false);
+ }
- var scoreInfo = score.ToScoreInfo(rulesets, working.BeatmapInfo);
+ if (includePinnedCheckbox.Current.Value)
+ {
+ var pinnedScores = await apiManager.GetJsonFromApi>($"users/{player.OnlineID}/scores/pinned?mode={ruleset.Value.ShortName}&limit={max_api_scores_in_one_query}")
+ .ConfigureAwait(false);
+ apiScores = apiScores.Concat(pinnedScores.Where(p => !apiScores.Any(b => b.ID == p.ID)).ToArray()).ToList();
+ }
- var parsedScore = new ProcessorScoreDecoder(working).Parse(scoreInfo);
+ foreach (var score in apiScores)
+ {
+ if (token.IsCancellationRequested)
+ return;
- var difficultyCalculator = rulesetInstance.CreateDifficultyCalculator(working);
- var difficultyAttributes = difficultyCalculator.Calculate(mods);
- var performanceCalculator = rulesetInstance.CreatePerformanceCalculator();
- if (performanceCalculator == null)
- continue;
+ var working = ProcessorWorkingBeatmap.FromFileOrId(score.BeatmapID.ToString(), cachePath: configManager.GetBindable(Settings.CachePath).Value);
- double? livePp = score.PP;
- var perfAttributes = await performanceCalculator.CalculateAsync(parsedScore.ScoreInfo, difficultyAttributes, token).ConfigureAwait(false);
- score.PP = perfAttributes.Total;
+ Schedule(() => loadingLayer.Text.Value = $"Calculating {working.Metadata}");
- var extendedScore = new ExtendedScore(score, livePp, perfAttributes);
- plays.Add(extendedScore);
+ Mod[] mods = score.Mods.Select(x => x.ToMod(rulesetInstance)).ToArray();
- Schedule(() => scores.Add(new ExtendedProfileScore(extendedScore)));
+ var scoreInfo = score.ToScoreInfo(rulesets, working.BeatmapInfo);
+
+ var parsedScore = new ProcessorScoreDecoder(working).Parse(scoreInfo);
+
+ var difficultyCalculator = rulesetInstance.CreateDifficultyCalculator(working);
+ var difficultyAttributes = difficultyCalculator.Calculate(mods);
+ var performanceCalculator = rulesetInstance.CreatePerformanceCalculator();
+ if (performanceCalculator == null)
+ continue;
+
+ double? livePp = score.PP;
+ var perfAttributes = await performanceCalculator.CalculateAsync(parsedScore.ScoreInfo, difficultyAttributes, token).ConfigureAwait(false);
+ score.PP = perfAttributes.Total;
+
+ var extendedScore = new ExtendedScore(score, livePp, perfAttributes);
+ plays.Add(extendedScore);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.Log(ex.ToString(), level: LogLevel.Error);
+ notificationDisplay.Display(new Notification($"Failed to calculate {username}: {ex.Message}"));
+ }
}
if (token.IsCancellationRequested)
return;
+ bool calculatingSingleProfile = players.Count == 1;
+
+ // Add user card if only calculating single profile
+ if (calculatingSingleProfile)
+ {
+ Schedule(() =>
+ {
+ userPanelContainer.Add(userPanel = new UserCard(players[0])
+ {
+ RelativeSizeAxes = Axes.X
+ });
+ });
+ }
+
+ // Filter plays if only displaying best score on each beatmap
+ if (onlyDisplayBestCheckbox.Current.Value)
+ {
+ Schedule(() => loadingLayer.Text.Value = "Filtering plays");
+
+ var filteredPlays = new List();
+
+ // 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();
@@ -322,6 +385,8 @@ namespace PerformanceCalculatorGUI.Screens
{
foreach (var play in plays)
{
+ scores.Add(new ExtendedProfileScore(play, !calculatingSingleProfile));
+
if (play.LivePP != null)
{
play.Position.Value = localOrdered.IndexOf(play) + 1;
@@ -330,29 +395,34 @@ namespace PerformanceCalculatorGUI.Screens
}
});
- decimal totalLocalPP = 0;
- for (int i = 0; i < localOrdered.Count; i++)
- totalLocalPP += (decimal)(Math.Pow(0.95, i) * (localOrdered[i].SoloScore.PP ?? 0));
-
- decimal totalLivePP = player.Statistics.PP ?? (decimal)0.0;
-
- decimal nonBonusLivePP = 0;
- for (int i = 0; i < liveOrdered.Count; i++)
- nonBonusLivePP += (decimal)(Math.Pow(0.95, i) * liveOrdered[i].LivePP ?? 0);
-
- //todo: implement properly. this is pretty damn wrong.
- decimal playcountBonusPP = (totalLivePP - nonBonusLivePP);
- totalLocalPP += playcountBonusPP;
-
- Schedule(() =>
+ if (calculatingSingleProfile)
{
- userPanel.Data.Value = new UserCardData
+ var player = players.First();
+
+ decimal totalLocalPP = 0;
+ for (int i = 0; i < localOrdered.Count; i++)
+ totalLocalPP += (decimal)(Math.Pow(0.95, i) * (localOrdered[i].SoloScore.PP ?? 0));
+
+ decimal totalLivePP = player.Statistics.PP ?? (decimal)0.0;
+
+ decimal nonBonusLivePP = 0;
+ for (int i = 0; i < liveOrdered.Count; i++)
+ nonBonusLivePP += (decimal)(Math.Pow(0.95, i) * liveOrdered[i].LivePP ?? 0);
+
+ //todo: implement properly. this is pretty damn wrong.
+ decimal 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);
diff --git a/PerformanceCalculatorGUI/Screens/Simulate/AttributesTable.cs b/PerformanceCalculatorGUI/Screens/Simulate/AttributesTable.cs
new file mode 100644
index 0000000..8016d60
--- /dev/null
+++ b/PerformanceCalculatorGUI/Screens/Simulate/AttributesTable.cs
@@ -0,0 +1,96 @@
+// Copyright (c) ppy Pty Ltd . 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 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.Overlays;
+using PerformanceCalculatorGUI.Components.TextBoxes;
+
+namespace PerformanceCalculatorGUI.Screens.Simulate
+{
+ public partial class AttributesTable : Container
+ {
+ public readonly Bindable> Attributes = new Bindable>();
+ private const float row_height = 35;
+
+ private FillFlowContainer backgroundFlow;
+ private GridContainer grid;
+
+ [Resolved]
+ private OverlayColourProvider colourProvider { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+
+ CornerRadius = ExtendedLabelledTextBox.CORNER_RADIUS;
+ Masking = true;
+
+ AddRangeInternal(new Drawable[]
+ {
+ backgroundFlow = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical
+ },
+ grid = new GridContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize), new Dimension() }
+ }
+ });
+
+ Attributes.BindValueChanged(onAttributesChanged);
+ }
+
+ private void onAttributesChanged(ValueChangedEvent> changedEvent)
+ {
+ grid.RowDimensions = Enumerable.Repeat(new Dimension(GridSizeMode.Absolute, row_height), changedEvent.NewValue.Count).ToArray();
+ grid.Content = changedEvent.NewValue.Select(s => createRowContent(s.Key, s.Value)).ToArray();
+
+ backgroundFlow.Children = changedEvent.NewValue.Select((_, i) => new Box
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = row_height,
+ Colour = colourProvider.Background4.Opacity(i % 2 == 0 ? 0.7f : 0.9f),
+ }).ToArray();
+ }
+
+ private Drawable[] createRowContent(string label, object value) => new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold),
+ Text = label.Humanize().ToLowerInvariant(),
+ Margin = new MarginPadding { Left = 15, Right = 10 },
+ UseFullGlyphHeight = true
+ },
+ new ReadonlyOsuTextBox(FormattableString.Invariant($"{value:N2}"), false)
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Height = 1,
+ RelativeSizeAxes = Axes.Both,
+ SelectAllOnFocus = true,
+ FontSize = 18,
+ CornerRadius = ExtendedLabelledTextBox.CORNER_RADIUS
+ },
+ }.ToArray();
+ }
+}
diff --git a/PerformanceCalculatorGUI/Screens/SimulateScreen.cs b/PerformanceCalculatorGUI/Screens/SimulateScreen.cs
index 42b71c9..735049d 100644
--- a/PerformanceCalculatorGUI/Screens/SimulateScreen.cs
+++ b/PerformanceCalculatorGUI/Screens/SimulateScreen.cs
@@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
-using Humanizer;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@@ -40,6 +39,7 @@ using PerformanceCalculatorGUI.Components;
using PerformanceCalculatorGUI.Components.TextBoxes;
using PerformanceCalculatorGUI.Configuration;
using PerformanceCalculatorGUI.Screens.ObjectInspection;
+using PerformanceCalculatorGUI.Screens.Simulate;
namespace PerformanceCalculatorGUI.Screens
{
@@ -71,10 +71,10 @@ namespace PerformanceCalculatorGUI.Screens
private SwitchButton fullScoreDataSwitch;
private DifficultyAttributes difficultyAttributes;
- private FillFlowContainer difficultyAttributesContainer;
- private FillFlowContainer performanceAttributesContainer;
+ private AttributesTable difficultyAttributesContainer;
private PerformanceCalculator performanceCalculator;
+ private AttributesTable performanceAttributesContainer;
[Cached]
private Bindable difficultyCalculator = new Bindable();
@@ -418,37 +418,23 @@ namespace PerformanceCalculatorGUI.Screens
{
new OsuSpriteText
{
- Margin = new MarginPadding { Left = 10f, Top = 5f, Bottom = 10.0f },
+ Margin = new MarginPadding { Left = 10f, Vertical = 5f },
Origin = Anchor.TopLeft,
Height = 20,
Text = "Difficulty Attributes"
},
- difficultyAttributesContainer = new FillFlowContainer
- {
- Direction = FillDirection.Vertical,
- RelativeSizeAxes = Axes.X,
- Anchor = Anchor.TopLeft,
- AutoSizeAxes = Axes.Y,
- Spacing = new Vector2(0, 2f)
- },
+ difficultyAttributesContainer = new AttributesTable(),
new OsuSpriteText
{
- Margin = new MarginPadding(10.0f),
+ Margin = new MarginPadding { Left = 10f, Vertical = 5f },
Origin = Anchor.TopLeft,
Height = 20,
Text = "Performance Attributes"
},
- performanceAttributesContainer = new FillFlowContainer
- {
- Direction = FillDirection.Vertical,
- RelativeSizeAxes = Axes.X,
- Anchor = Anchor.TopLeft,
- AutoSizeAxes = Axes.Y,
- Spacing = new Vector2(0, 2f)
- },
+ performanceAttributesContainer = new AttributesTable(),
new OsuSpriteText
{
- Margin = new MarginPadding(10.0f),
+ Margin = new MarginPadding { Left = 10f, Vertical = 5f },
Origin = Anchor.TopLeft,
Height = 20,
Text = "Strain graph (alt+scroll to zoom)"
@@ -582,6 +568,7 @@ namespace PerformanceCalculatorGUI.Screens
// recreate calculators to update DHOs
createCalculators();
+ modSettingChangeTracker?.Dispose();
modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue);
modSettingChangeTracker.SettingChanged += m =>
{
@@ -592,7 +579,7 @@ namespace PerformanceCalculatorGUI.Screens
updateMissesTextboxes();
calculateDifficulty();
calculatePerformance();
- }, 100);
+ }, 300);
};
calculateDifficulty();
@@ -647,6 +634,7 @@ namespace PerformanceCalculatorGUI.Screens
resetCalculations();
}
+ beatmapTitle.Clear();
beatmapTitle.Add(new BeatmapCard(working));
loadBackground();
@@ -672,14 +660,7 @@ namespace PerformanceCalculatorGUI.Screens
try
{
difficultyAttributes = difficultyCalculator.Value.Calculate(appliedMods.Value);
- difficultyAttributesContainer.Children = AttributeConversion.ToDictionary(difficultyAttributes).Select(x =>
- new ExtendedLabelledTextBox
- {
- ReadOnly = true,
- Label = x.Key.Humanize().ToLowerInvariant(),
- Text = FormattableString.Invariant($"{x.Value:N2}")
- }
- ).ToArray();
+ difficultyAttributesContainer.Attributes.Value = AttributeConversion.ToDictionary(difficultyAttributes);
}
catch (Exception e)
{
@@ -752,17 +733,11 @@ namespace PerformanceCalculatorGUI.Screens
Statistics = statistics,
Mods = appliedMods.Value.ToArray(),
TotalScore = score,
- Ruleset = ruleset.Value
+ Ruleset = ruleset.Value,
+ LegacyTotalScore = legacyTotalScore,
}, difficultyAttributes);
- performanceAttributesContainer.Children = AttributeConversion.ToDictionary(ppAttributes).Select(x =>
- new ExtendedLabelledTextBox
- {
- ReadOnly = true,
- Label = x.Key.Humanize().ToLowerInvariant(),
- Text = FormattableString.Invariant($"{x.Value:N2}")
- }
- ).ToArray();
+ performanceAttributesContainer.Attributes.Value = AttributeConversion.ToDictionary(ppAttributes);
}
catch (Exception e)
{
@@ -920,7 +895,10 @@ namespace PerformanceCalculatorGUI.Screens
private void resetCalculations()
{
createCalculators();
+
resetMods();
+ legacyTotalScore = null;
+
calculateDifficulty();
calculatePerformance();
populateScoreParams();
@@ -1011,6 +989,8 @@ namespace PerformanceCalculatorGUI.Screens
notificationDisplay.Display(new Notification(message));
}
+ private long? legacyTotalScore;
+
private void populateSettingsFromScore(long scoreId)
{
if (scoreIdPopulateButton.State.Value == ButtonState.Loading)
@@ -1035,6 +1015,8 @@ namespace PerformanceCalculatorGUI.Screens
ruleset.Value = rulesets.GetRuleset(scoreInfo.RulesetID);
appliedMods.Value = scoreInfo.Mods.Select(x => x.ToMod(ruleset.Value.CreateInstance())).ToList();
+ legacyTotalScore = scoreInfo.LegacyTotalScore;
+
fullScoreDataSwitch.Current.Value = true;
// TODO: this shouldn't be done in 2 lines
@@ -1062,6 +1044,21 @@ namespace PerformanceCalculatorGUI.Screens
mehsTextBox.Text = mehs.ToString();
}
+ if (ruleset.Value?.ShortName == "fruits")
+ {
+ if (scoreInfo.Statistics.TryGetValue(HitResult.LargeTickHit, out int largeTickHits))
+ {
+ goodsTextBox.Value.Value = largeTickHits;
+ goodsTextBox.Text = largeTickHits.ToString();
+ }
+
+ if (scoreInfo.Statistics.TryGetValue(HitResult.SmallTickHit, out int smallTickHits))
+ {
+ mehsTextBox.Value.Value = smallTickHits;
+ mehsTextBox.Text = smallTickHits.ToString();
+ }
+ }
+
if (scoreInfo.Statistics.TryGetValue(HitResult.LargeTickMiss, out int largeTickMisses))
{
largeTickMissesTextBox.Value.Value = largeTickMisses;