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;