diff --git a/PerformanceCalculatorGUI/Components/ExtendedProfileScore.cs b/PerformanceCalculatorGUI/Components/ExtendedProfileScore.cs index e8aacd4e2..fb0a51d83 100644 --- a/PerformanceCalculatorGUI/Components/ExtendedProfileScore.cs +++ b/PerformanceCalculatorGUI/Components/ExtendedProfileScore.cs @@ -1,423 +1,115 @@ // 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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; -using osu.Framework.Localisation; -using osu.Framework.Platform; -using osu.Game.Beatmaps; using osu.Game.Graphics; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Leaderboards; -using osu.Game.Overlays; -using osu.Game.Overlays.Profile.Sections; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; -using osu.Game.Rulesets.Scoring; -using osu.Game.Rulesets.UI; -using osu.Game.Utils; -using osuTK; -using PerformanceCalculatorGUI.Components.TextBoxes; namespace PerformanceCalculatorGUI.Components { - public class ExtendedScore + public class ExtendedProfileScore : ProfileScore { - public SoloScoreInfo SoloScore { get; } public double LivePP { get; } - - public Bindable Position { get; } = new Bindable(); public Bindable PositionChange { get; } = new Bindable(); - public PerformanceAttributes PerformanceAttributes { get; } - - public ExtendedScore(SoloScoreInfo score, double livePP, PerformanceAttributes attributes) + public ExtendedProfileScore(SoloScoreInfo score, double livePP, PerformanceAttributes attributes) + : base(score, attributes) { - SoloScore = score; - PerformanceAttributes = attributes; LivePP = livePP; } } - public partial class ExtendedProfileItemContainer : ProfileItemContainer + public partial class DrawableExtendedProfileScore : DrawableProfileScore { - public Action OnHoverAction { get; set; } - public Action OnUnhoverAction { get; set; } - - public ExtendedProfileItemContainer() - { - CornerRadius = ExtendedLabelledTextBox.CORNER_RADIUS; - } - - protected override bool OnHover(HoverEvent e) - { - OnHoverAction?.Invoke(); - return base.OnHover(e); - } + protected new ExtendedProfileScore Score { get; } - protected override void OnHoverLost(HoverLostEvent e) + public DrawableExtendedProfileScore(ExtendedProfileScore score) + : base(score) { - OnUnhoverAction?.Invoke(); - base.OnHoverLost(e); + Score = score; } - } - - public partial class ExtendedProfileScore : CompositeDrawable - { - private const int height = 40; - private const int performance_width = 100; - private const int rank_difference_width = 35; - private const int small_text_font_size = 11; - - private const float performance_background_shear = 0.45f; - - protected readonly ExtendedScore Score; - - [Resolved] - private OsuColour colours { get; set; } - - [Resolved] - private OverlayColourProvider colourProvider { get; set; } - private OsuSpriteText positionChangeText; - - public ExtendedProfileScore(ExtendedScore score) + protected override void LoadComplete() { - Score = score; + base.LoadComplete(); - RelativeSizeAxes = Axes.X; - Height = height; + Score.Position.UnbindEvents(); + Score.PositionChange.BindValueChanged(v => { PositionText.Text = $"{v.NewValue:+0;-0;-}"; }); } - [BackgroundDependencyLoader] - private void load(RulesetStore rulesets) + protected override Drawable[] CreateRightInfoContainerContent(RulesetStore rulesets) { - AddInternal(new ExtendedProfileItemContainer + return new Drawable[] { - OnHoverAction = () => - { - positionChangeText.Text = $"#{Score.Position.Value}"; - }, - OnUnhoverAction = () => + new FillFlowContainer { - positionChangeText.Text = $"{Score.PositionChange.Value:+0;-0;-}"; - }, - Children = new Drawable[] - { - new Container - { - Name = "Rank difference", - RelativeSizeAxes = Axes.Y, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Width = rank_difference_width, - Child = positionChangeText = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = colourProvider.Light1, - Text = Score.PositionChange.Value.ToString() - } - }, - new Container + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Width = 60, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] { - Name = "Score info", - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = rank_difference_width, Right = performance_width }, - Children = new Drawable[] + new Container { - new FillFlowContainer + AutoSizeAxes = Axes.Y, + Child = new OsuSpriteText { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Children = new Drawable[] - { - new UpdateableRank(Score.SoloScore.Rank) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(50, 20), - }, - new FillFlowContainer - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 0.5f), - Children = new Drawable[] - { - new ScoreBeatmapMetadataContainer(Score.SoloScore.Beatmap), - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(15, 0), - Children = new Drawable[] - { - new OsuSpriteText - { - Text = $"{Score.SoloScore.Beatmap?.DifficultyName}", - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular), - Colour = colours.Yellow - }, - new DrawableDate(Score.SoloScore.EndedAt, 12) - { - Colour = colourProvider.Foreground1 - } - } - } - } - } - } + Font = OsuFont.GetFont(weight: FontWeight.Bold), + Text = $"{Score.LivePP:0}pp" }, - new FillFlowContainer - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - Direction = FillDirection.Horizontal, - Children = new Drawable[] - { - new Container - { - AutoSizeAxes = Axes.X, - RelativeSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = 10, Vertical = 5 }, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Child = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Children = new Drawable[] - { - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Children = new Drawable[] - { - new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 110, - RelativeSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new OsuSpriteText - { - Text = Score.SoloScore.Accuracy.FormatAccuracy(), - Font = OsuFont.GetFont(weight: FontWeight.Bold, italics: true), - Colour = colours.Yellow, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre - }, - new OsuSpriteText - { - Text = $"{Score.SoloScore.MaxCombo}x {{ {formatStatistics(Score.SoloScore.Statistics)} }}", - Font = OsuFont.GetFont(size: small_text_font_size, weight: FontWeight.Regular), - Colour = colourProvider.Light2, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre - }, - } - }, - new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 60, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new Container - { - AutoSizeAxes = Axes.Y, - Child = new OsuSpriteText - { - Font = OsuFont.GetFont(weight: FontWeight.Bold), - Text = $"{Score.LivePP:0}pp" - }, - }, - new OsuSpriteText - { - Font = OsuFont.GetFont(size: small_text_font_size), - Text = "live" - } - } - } - } - } - } - } - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(2), - Children = Score.SoloScore.Mods.Select(mod => - { - var ruleset = rulesets.GetRuleset(Score.SoloScore.RulesetID) ?? throw new InvalidOperationException(); - - return new ModIcon(ruleset.CreateInstance().CreateModFromAcronym(mod.Acronym)!) - { - Scale = new Vector2(0.35f) - }; - }).ToList(), - } - } - } - } - }, - new Container - { - Name = "Performance", - RelativeSizeAxes = Axes.Y, - Width = performance_width, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Children = new Drawable[] + }, + new OsuSpriteText { - new Box - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - RelativeSizeAxes = Axes.Both, - Height = 0.5f, - Colour = colourProvider.Background4, - Shear = new Vector2(-performance_background_shear, 0), - EdgeSmoothness = new Vector2(2, 0), - }, - new Box - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - RelativeSizeAxes = Axes.Both, - RelativePositionAxes = Axes.Y, - Height = -0.5f, - Position = new Vector2(0, 1), - Colour = colourProvider.Background4, - Shear = new Vector2(performance_background_shear, 0), - EdgeSmoothness = new Vector2(2, 0), - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Padding = new MarginPadding - { - Vertical = 5, - Left = 30, - Right = 20 - }, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new ExtendedOsuSpriteText - { - Font = OsuFont.GetFont(weight: FontWeight.Bold), - Text = $"{Score.SoloScore.PP:0}pp", - Colour = colourProvider.Highlight1, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - TooltipContent = $"{AttributeConversion.ToReadableString(Score.PerformanceAttributes)}" - }, - new OsuSpriteText - { - Font = OsuFont.GetFont(size: small_text_font_size), - Text = $"{Score.SoloScore.PP - Score.LivePP:+0.0;-0.0;-}", - Colour = colourProvider.Light1, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre - } - } - } + Font = OsuFont.GetFont(size: SMALL_TEXT_FONT_SIZE), + Text = "live" } } } - }); - - Score.PositionChange.BindValueChanged(v => { positionChangeText.Text = $"{v.NewValue:+0;-0;-}"; }); - } - - private static string formatStatistics(Dictionary statistics) - { - // TODO: ruleset-specific display - return - $"{statistics.GetValueOrDefault(HitResult.Great)} / {statistics.GetValueOrDefault(HitResult.Ok)} / {statistics.GetValueOrDefault(HitResult.Meh)} / {statistics.GetValueOrDefault(HitResult.Miss)}"; + }.Concat(base.CreateRightInfoContainerContent(rulesets)).ToArray(); } - private partial class ScoreBeatmapMetadataContainer : OsuHoverContainer + protected override Drawable CreatePerformanceInfo() { - private readonly IBeatmapInfo beatmapInfo; - - public ScoreBeatmapMetadataContainer(IBeatmapInfo beatmapInfo) - { - this.beatmapInfo = beatmapInfo; - AutoSizeAxes = Axes.Both; - } - - [BackgroundDependencyLoader(true)] - private void load(GameHost host) + return new FillFlowContainer { - Action = () => + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding { - host.OpenUrlExternally($"https://osu.ppy.sh/b/{beatmapInfo.OnlineID}"); - }; - - Child = new FillFlowContainer + Vertical = 5, + Left = 30, + Right = 20 + }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Vertical, + Children = new Drawable[] { - AutoSizeAxes = Axes.Both, - Children = new Drawable[] + new ExtendedOsuSpriteText { - new OsuSpriteText - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Text = new RomanisableString(beatmapInfo.Metadata.TitleUnicode, beatmapInfo.Metadata.Title), - Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold, italics: true) - }, - new OsuSpriteText - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Text = " by ", - Font = OsuFont.GetFont(size: 12, italics: true) - }, - new OsuSpriteText - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Text = new RomanisableString(beatmapInfo.Metadata.ArtistUnicode, beatmapInfo.Metadata.Artist), - Font = OsuFont.GetFont(size: 12, italics: true) - }, + Font = OsuFont.GetFont(weight: FontWeight.Bold), + Text = $"{Score.SoloScore.PP:0}pp", + Colour = ColourProvider.Highlight1, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + TooltipContent = $"{AttributeConversion.ToReadableString(Score.PerformanceAttributes)}" + }, + new OsuSpriteText + { + Font = OsuFont.GetFont(size: SMALL_TEXT_FONT_SIZE), + Text = $"{Score.SoloScore.PP - Score.LivePP:+0.0;-0.0;-}", + Colour = ColourProvider.Light1, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre } - }; - } + } + }; } } } diff --git a/PerformanceCalculatorGUI/Components/LazerCalculationSettings.cs b/PerformanceCalculatorGUI/Components/LazerCalculationSettings.cs new file mode 100644 index 000000000..ca85a3944 --- /dev/null +++ b/PerformanceCalculatorGUI/Components/LazerCalculationSettings.cs @@ -0,0 +1,138 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osuTK; +using osu.Framework.Extensions; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osu.Game.Overlays.Toolbar; +using osu.Framework.Bindables; +using osu.Game.Scoring; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Osu.Mods; + +namespace PerformanceCalculatorGUI.Components +{ + public partial class LazerCalculationSettings : ToolbarButton, IHasPopover + { + public readonly Bindable CalculateRankedMaps = new Bindable(true); + public readonly Bindable CalculateUnrankedMaps = new Bindable(false); + + public readonly Bindable CalculateUnsubmittedScores = new Bindable(true); + public readonly Bindable CalculateUnrankedMods = new Bindable(true); + + public readonly Bindable EnableScorev1Overwrite = new Bindable(false); + + public bool IsScorev1OverwritingEnabled => EnableScorev1Overwrite.Value; + + protected override Anchor TooltipAnchor => Anchor.TopRight; + + public LazerCalculationSettings() + { + TooltipMain = "Calculation Settings"; + + SetIcon(new ScreenSelectionButtonIcon(FontAwesome.Solid.Cog) { IconSize = new Vector2(70) }); + } + + public bool ShouldBeFiltered(ScoreInfo score) + { + if (score.BeatmapInfo == null) + return true; + + if (!CalculateRankedMaps.Value && score.BeatmapInfo.Status.GrantsPerformancePoints()) + return true; + + if (!CalculateUnrankedMaps.Value && !score.BeatmapInfo.Status.GrantsPerformancePoints()) + return true; + + if (!CalculateUnrankedMods.Value) + { + // Check for legacy score because CL is unranked + if (!score.Mods.All(m => m.Ranked || (score.IsLegacyScore && m is OsuModClassic))) + return true; + } + + if (!CalculateUnsubmittedScores.Value) + { + if (score.OnlineID <= 0 && score.LegacyOnlineID <= 0) + return true; + } + + return false; + } + + public Popover GetPopover() => new LazerCalculationSettingsPopover(this); + + protected override bool OnClick(ClickEvent e) + { + this.ShowPopover(); + return base.OnClick(e); + } + } + + public partial class LazerCalculationSettingsPopover : OsuPopover + { + private readonly LazerCalculationSettings parent; + + public LazerCalculationSettingsPopover(LazerCalculationSettings parent) + { + this.parent = parent; + } + + [BackgroundDependencyLoader] + private void load() + { + Add(new Container + { + AutoSizeAxes = Axes.Y, + Width = 500, + Children = new Drawable[] + { + new FillFlowContainer + { + Direction = FillDirection.Vertical, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(18), + Children = new Drawable[] + { + new OsuCheckbox + { + LabelText = "Calculate Ranked Maps", + Current = { BindTarget = parent.CalculateRankedMaps } + }, + new OsuCheckbox + { + LabelText = "Calculate Unranked Maps", + Current = { BindTarget = parent.CalculateUnrankedMaps } + }, + new OsuCheckbox + { + LabelText = "Calculate Unsubmitted Scores, such as scores set on local difficulties", + Current = { BindTarget = parent.CalculateUnsubmittedScores } + }, + new OsuCheckbox + { + LabelText = "Calculate Unranked Mods, Autopilot is excluded regardless", + Current = { BindTarget = parent.CalculateUnsubmittedScores } + }, + new OsuCheckbox + { + LabelText = "Enable Scorev1 score overwrite for legacy scores", + Current = { BindTarget = parent.EnableScorev1Overwrite } + }, + } + } + } + }); + } + } +} diff --git a/PerformanceCalculatorGUI/Components/ProfileScore.cs b/PerformanceCalculatorGUI/Components/ProfileScore.cs new file mode 100644 index 000000000..0b42859c2 --- /dev/null +++ b/PerformanceCalculatorGUI/Components/ProfileScore.cs @@ -0,0 +1,380 @@ +// 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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; +using osu.Framework.Platform; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Leaderboards; +using osu.Game.Overlays; +using osu.Game.Overlays.Profile.Sections; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; +using osu.Game.Scoring; +using osu.Game.Utils; +using osuTK; +using PerformanceCalculatorGUI.Components.TextBoxes; + +namespace PerformanceCalculatorGUI.Components +{ + public class ProfileScore + { + public SoloScoreInfo SoloScore { get; } + + public Bindable Position { get; } = new Bindable(); + + public PerformanceAttributes PerformanceAttributes { get; } + + public ProfileScore(SoloScoreInfo score, PerformanceAttributes attributes) + { + SoloScore = score; + PerformanceAttributes = attributes; + } + + public ProfileScore(ScoreInfo score, PerformanceAttributes attributes) + { + SoloScore = toSoloScoreInfo(score); + PerformanceAttributes = attributes; + } + + private static SoloScoreInfo toSoloScoreInfo(ScoreInfo score) + { + APIBeatmapSet dummySet = new APIBeatmapSet + { + Title = score.BeatmapInfo?.Metadata.Title ?? "Unknown Title", + TitleUnicode = score.BeatmapInfo?.Metadata.TitleUnicode ?? "Unknown Title", + Artist = score.BeatmapInfo?.Metadata.Artist ?? "Unknown Artist", + ArtistUnicode = score.BeatmapInfo?.Metadata.ArtistUnicode ?? "Unknown Artist", + }; + APIBeatmap dummyBeatmap = new APIBeatmap + { + OnlineID = score.BeatmapInfo?.OnlineID ?? 0, + DifficultyName = score.BeatmapInfo?.DifficultyName ?? "Unknown Difficulty", + }; + SoloScoreInfo soloScoreInfo = new SoloScoreInfo + { + PP = score.PP, + Accuracy = score.Accuracy, + Rank = score.Rank, + Statistics = score.Statistics, + MaxCombo = score.MaxCombo, + Mods = score.APIMods, + Beatmap = dummyBeatmap, + EndedAt = score.Date, + BeatmapSet = dummySet, + }; + + return soloScoreInfo; + } + } + + public partial class DrawableProfileScore : CompositeDrawable + { + private const int height = 40; + private const int rank_difference_width = 35; + private const int performance_width = 100; + private const int rank_width = 35; + + protected const int SMALL_TEXT_FONT_SIZE = 11; + + private const float performance_background_shear = 0.45f; + + protected FillFlowContainer RightInfoContainer; + + protected ProfileScore Score { get; } + + [Resolved] + private OsuColour colours { get; set; } + + [Resolved] + protected OverlayColourProvider ColourProvider { get; private set; } + + protected OsuSpriteText PositionText; + + public DrawableProfileScore(ProfileScore score) + { + Score = score; + + RelativeSizeAxes = Axes.X; + Height = height; + } + + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) + { + AddInternal(new ProfileItemContainer + { + RelativeSizeAxes = Axes.Both, + CornerRadius = ExtendedLabelledTextBox.CORNER_RADIUS, + Children = new Drawable[] + { + new Container + { + Name = "Rank difference", + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = rank_width, + Children = new Drawable[] + { + PositionText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = ColourProvider.Light1, + Text = Score.Position.Value.ToString() + } + } + }, + new Container + { + Name = "Score info", + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = rank_difference_width, Right = performance_width }, + Children = new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + new UpdateableRank(Score.SoloScore.Rank) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(50, 20), + }, + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 0.5f), + Children = new Drawable[] + { + new ScoreBeatmapMetadataContainer(Score.SoloScore.Beatmap), + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(15, 0), + Children = new Drawable[] + { + new OsuSpriteText + { + Text = $"{Score.SoloScore.Beatmap?.DifficultyName}", + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular), + Colour = colours.Yellow + }, + new DrawableDate(Score.SoloScore.EndedAt, 12) + { + Colour = ColourProvider.Foreground1 + } + } + } + } + } + } + }, + RightInfoContainer = new FillFlowContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Direction = FillDirection.Horizontal, + Padding = new MarginPadding { Right = 10 }, + Children = CreateRightInfoContainerContent(rulesets) + } + } + }, + new Container + { + Name = "Performance", + RelativeSizeAxes = Axes.Y, + Width = performance_width, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Children = new[] + { + new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Both, + Height = 0.5f, + Colour = ColourProvider.Background4, + Shear = new Vector2(-performance_background_shear, 0), + EdgeSmoothness = new Vector2(2, 0), + }, + new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.Y, + Height = -0.5f, + Position = new Vector2(0, 1), + Colour = ColourProvider.Background4, + Shear = new Vector2(performance_background_shear, 0), + EdgeSmoothness = new Vector2(2, 0), + }, + CreatePerformanceInfo() + } + } + } + }); + } + + protected virtual Drawable[] CreateRightInfoContainerContent(RulesetStore rulesets) + { + return new Drawable[] + { + new Container + { + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = 10, Vertical = 5 }, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 110, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new OsuSpriteText + { + Text = Score.SoloScore.Accuracy.FormatAccuracy(), + Font = OsuFont.GetFont(weight: FontWeight.Bold, italics: true), + Colour = colours.Yellow, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + }, + new OsuSpriteText + { + Text = $"{Score.SoloScore.MaxCombo}x {{ {formatStatistics(Score.SoloScore.Statistics)} }}", + Font = OsuFont.GetFont(size: SMALL_TEXT_FONT_SIZE, weight: FontWeight.Regular), + Colour = ColourProvider.Light2, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + }, + } + } + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(2), + Children = Score.SoloScore.Mods.Select(mod => + { + var ruleset = rulesets.GetRuleset(Score.SoloScore.RulesetID) ?? throw new InvalidOperationException(); + return new ModIcon(mod.ToMod(ruleset.CreateInstance())) + { + Scale = new Vector2(0.35f) + }; + }).ToList(), + } + }; + } + + protected virtual Drawable CreatePerformanceInfo() + { + return new ExtendedOsuSpriteText + { + Padding = new MarginPadding + { + Vertical = 5, + Left = 30, + Right = 20 + }, + Font = OsuFont.GetFont(weight: FontWeight.Bold), + Text = $"{Score.SoloScore.PP:0}pp", + Colour = ColourProvider.Highlight1, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + TooltipContent = $"{AttributeConversion.ToReadableString(Score.PerformanceAttributes)}" + }; + } + + private static string formatStatistics(Dictionary statistics) + { + // TODO: ruleset-specific display + return + $"{statistics.GetValueOrDefault(HitResult.Great)} / {statistics.GetValueOrDefault(HitResult.Ok)} / {statistics.GetValueOrDefault(HitResult.Meh)} / {statistics.GetValueOrDefault(HitResult.Miss)}"; + } + + private partial class ScoreBeatmapMetadataContainer : OsuHoverContainer + { + private readonly IBeatmapInfo beatmapInfo; + + public ScoreBeatmapMetadataContainer(IBeatmapInfo beatmapInfo) + { + this.beatmapInfo = beatmapInfo; + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader(true)] + private void load(GameHost host) + { + Action = () => + { + host.OpenUrlExternally($"https://osu.ppy.sh/b/{beatmapInfo.OnlineID}"); + }; + + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Text = new RomanisableString(beatmapInfo.Metadata.TitleUnicode, beatmapInfo.Metadata.Title), + Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold, italics: true) + }, + new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Text = " by ", + Font = OsuFont.GetFont(size: 12, italics: true) + }, + new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Text = new RomanisableString(beatmapInfo.Metadata.ArtistUnicode, beatmapInfo.Metadata.Artist), + Font = OsuFont.GetFont(size: 12, italics: true) + }, + } + }; + } + } + } +} diff --git a/PerformanceCalculatorGUI/Components/SettingsPopover.cs b/PerformanceCalculatorGUI/Components/SettingsPopover.cs index 8cca41cab..6f43b4ec0 100644 --- a/PerformanceCalculatorGUI/Components/SettingsPopover.cs +++ b/PerformanceCalculatorGUI/Components/SettingsPopover.cs @@ -28,6 +28,7 @@ public partial class SettingsPopover : OsuPopover private Bindable clientSecretBindable; private Bindable pathBindable; private Bindable cacheBindable; + private Bindable lazerPathBindable; private Bindable scaleBindable; private const string api_key_link = "https://osu.ppy.sh/home/account/edit#new-oauth-application"; @@ -40,6 +41,7 @@ private void load(SettingsManager configManager, OsuConfigManager osuConfig) clientSecretBindable = configManager.GetBindable(Settings.ClientSecret); pathBindable = configManager.GetBindable(Settings.DefaultPath); cacheBindable = configManager.GetBindable(Settings.CachePath); + lazerPathBindable = configManager.GetBindable(Settings.LazerFolderPath); scaleBindable = osuConfig.GetBindable(OsuSetting.UIScale); Add(new Container @@ -95,6 +97,12 @@ private void load(SettingsManager configManager, OsuConfigManager osuConfig) Label = "Beatmap cache path", Current = { BindTarget = cacheBindable } }, + new LabelledTextBox + { + RelativeSizeAxes = Axes.X, + Label = "Lazer folder path", + Current = { BindTarget = lazerPathBindable } + }, new Box { RelativeSizeAxes = Axes.X, diff --git a/PerformanceCalculatorGUI/Configuration/SettingsManager.cs b/PerformanceCalculatorGUI/Configuration/SettingsManager.cs index 19e0447c0..506430cad 100644 --- a/PerformanceCalculatorGUI/Configuration/SettingsManager.cs +++ b/PerformanceCalculatorGUI/Configuration/SettingsManager.cs @@ -13,7 +13,8 @@ public enum Settings ClientId, ClientSecret, DefaultPath, - CachePath + CachePath, + LazerFolderPath } public class SettingsManager : IniConfigManager @@ -31,6 +32,7 @@ protected override void InitialiseDefaults() SetDefault(Settings.ClientSecret, string.Empty); SetDefault(Settings.DefaultPath, Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)); SetDefault(Settings.CachePath, Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "cache")); + SetDefault(Settings.LazerFolderPath, string.Empty); } } } diff --git a/PerformanceCalculatorGUI/RulesetHelper.cs b/PerformanceCalculatorGUI/RulesetHelper.cs index c99ad0b9d..313fddf60 100644 --- a/PerformanceCalculatorGUI/RulesetHelper.cs +++ b/PerformanceCalculatorGUI/RulesetHelper.cs @@ -26,10 +26,9 @@ namespace PerformanceCalculatorGUI public static class RulesetHelper { /// - /// Transforms a given combination into one which is applicable to legacy scores. - /// This is used to match osu!stable/osu!web calculations for the time being, until such a point that these mods do get considered. + /// Creates hashset of types for each of that's affecting difficulty /// - public static Mod[] ConvertToLegacyDifficultyAdjustmentMods(Ruleset ruleset, Mod[] mods) + public static HashSet GetDifficultyAdjustingModsHashSet(Ruleset ruleset) { var beatmap = new EmptyWorkingBeatmap { @@ -42,15 +41,30 @@ public static Mod[] ConvertToLegacyDifficultyAdjustmentMods(Ruleset ruleset, Mod var allMods = ruleset.CreateAllMods().ToArray(); - var allowedMods = ModUtils.FlattenMods( + var difficultyAdjustingMods = ModUtils.FlattenMods( ruleset.CreateDifficultyCalculator(beatmap).CreateDifficultyAdjustmentModCombinations()) .Select(m => m.GetType()) .Distinct() .ToHashSet(); - // Special case to allow either DT or NC. - if (allowedMods.Any(type => type.IsSubclassOf(typeof(ModDoubleTime))) && mods.Any(m => m is ModNightcore)) - allowedMods.Add(allMods.Single(m => m is ModNightcore).GetType()); + // Explicitly add NC and DC as they're not returned in CreateDifficultyAdjustmentModCombinations for some reason + if (difficultyAdjustingMods.Any(type => type.IsSubclassOf(typeof(ModDoubleTime)))) + difficultyAdjustingMods.Add(allMods.Single(m => m is ModNightcore).GetType()); + + if (difficultyAdjustingMods.Any(type => type.IsSubclassOf(typeof(ModHalfTime)))) + difficultyAdjustingMods.Add(allMods.Single(m => m is ModDaycore).GetType()); + + return difficultyAdjustingMods; + } + + /// + /// Transforms a given combination into one which is applicable to legacy scores. + /// This is used to match osu!stable/osu!web calculations for the time being, until such a point that these mods do get considered. + /// + public static Mod[] ConvertToLegacyDifficultyAdjustmentMods(Ruleset ruleset, Mod[] mods) + { + var allMods = ruleset.CreateAllMods().ToArray(); + var allowedMods = GetDifficultyAdjustingModsHashSet(ruleset); var result = new List(); diff --git a/PerformanceCalculatorGUI/Screens/BeatmapLeaderboardScreen.cs b/PerformanceCalculatorGUI/Screens/BeatmapLeaderboardScreen.cs index 00a8334f5..b5bb5a4f6 100644 --- a/PerformanceCalculatorGUI/Screens/BeatmapLeaderboardScreen.cs +++ b/PerformanceCalculatorGUI/Screens/BeatmapLeaderboardScreen.cs @@ -219,6 +219,8 @@ private void calculate() if (leaderboard.Scores.Count == 0) return; + var difficultyCalculator = rulesetInstance.CreateDifficultyCalculator(working); + foreach (var score in leaderboard.Scores) { if (token.IsCancellationRequested) @@ -227,14 +229,12 @@ private void calculate() Schedule(() => loadingLayer.Text.Value = $"Calculating {score.User?.Username}"); 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 difficultyCalculator = rulesetInstance.CreateDifficultyCalculator(working); - - Mod[] mods = score.Mods.Select(x => x.ToMod(rulesetInstance)).ToArray(); - var difficultyAttributes = difficultyCalculator.Calculate(mods); + var performanceCalculator = rulesetInstance.CreatePerformanceCalculator(); var perfAttributes = await performanceCalculator?.CalculateAsync(parsedScore.ScoreInfo, difficultyAttributes, token)!; diff --git a/PerformanceCalculatorGUI/Screens/LeaderboardScreen.cs b/PerformanceCalculatorGUI/Screens/LeaderboardScreen.cs index c61a41c46..aeea63733 100644 --- a/PerformanceCalculatorGUI/Screens/LeaderboardScreen.cs +++ b/PerformanceCalculatorGUI/Screens/LeaderboardScreen.cs @@ -31,7 +31,7 @@ public class UserLeaderboardData public decimal LivePP { get; set; } public decimal LocalPP { get; set; } - public List Scores { get; set; } + public List Scores { get; set; } } public partial class LeaderboardScreen : PerformanceCalculatorScreen @@ -242,7 +242,7 @@ private void calculate() var leaderboard = await apiManager.GetJsonFromApi($"rankings/{ruleset.Value.ShortName}/performance?cursor[page]={pageTextBox.Value.Value - 1}"); var calculatedPlayers = new List(); - var calculatedScores = new List(); + var calculatedScores = new List(); for (int i = 0; i < playerAmountTextBox.Value.Value; i++) { @@ -277,7 +277,7 @@ private void calculate() foreach (var calculatedScore in calculatedScores.OrderByDescending(x => x.PerformanceAttributes.Total)) { - scores.Add(new ExtendedProfileScore(calculatedScore)); + scores.Add(new DrawableExtendedProfileScore(calculatedScore)); } }); }, token).ContinueWith(t => @@ -299,7 +299,7 @@ private async Task calculatePlayer(UserStatistics player, C if (token.IsCancellationRequested) return new UserLeaderboardData(); - var plays = new List(); + var plays = new List(); var apiScores = await apiManager.GetJsonFromApi>($"users/{player.User.OnlineID}/scores/best?mode={ruleset.Value.ShortName}&limit=100"); @@ -327,7 +327,7 @@ private async Task calculatePlayer(UserStatistics player, C var perfAttributes = performanceCalculator?.Calculate(parsedScore.ScoreInfo, difficultyAttributes); score.PP = perfAttributes?.Total ?? 0.0; - var extendedScore = new ExtendedScore(score, livePp, perfAttributes); + var extendedScore = new ExtendedProfileScore(score, livePp, perfAttributes); plays.Add(extendedScore); } catch (Exception e) diff --git a/PerformanceCalculatorGUI/Screens/ProfileScreen.cs b/PerformanceCalculatorGUI/Screens/ProfileScreen.cs index e46a56381..b5a4d028e 100644 --- a/PerformanceCalculatorGUI/Screens/ProfileScreen.cs +++ b/PerformanceCalculatorGUI/Screens/ProfileScreen.cs @@ -6,22 +6,28 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; +using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; +using osu.Game.Scoring; using osuTK.Graphics; using PerformanceCalculatorGUI.Components; using PerformanceCalculatorGUI.Components.TextBoxes; using PerformanceCalculatorGUI.Configuration; +using System.IO; +using osu.Framework.Platform; +using osu.Game.Screens.Play; namespace PerformanceCalculatorGUI.Screens { @@ -30,7 +36,6 @@ public partial class ProfileScreen : PerformanceCalculatorScreen [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); - private StatefulButton calculationButton; private VerboseLoadingLayer loadingLayer; private GridContainer layout; @@ -41,7 +46,16 @@ public partial class ProfileScreen : PerformanceCalculatorScreen private Container userPanelContainer; private UserCard userPanel; - private string currentUser; + private GridContainer setupContainer; + private SwitchButton profileImportTypeSwitch; + + private StatefulButton calculationButtonServer; + + private GridContainer localCalcSetupContainer; + private StatefulButton calculationButtonLocal; + private LazerCalculationSettings settingsMenu; + + private string[] currentUserNicknames; private CancellationTokenSource calculationCancellatonToken; @@ -60,8 +74,12 @@ public partial class ProfileScreen : PerformanceCalculatorScreen [Resolved] private RulesetStore rulesets { get; set; } + [Resolved] + private GameHost gameHost { get; set; } + public override bool ShouldShowConfirmationDialogOnSwitch => false; + private const float setup_width = 220; private const float username_container_height = 40; public ProfileScreen() @@ -72,6 +90,41 @@ public ProfileScreen() [BackgroundDependencyLoader] private void load() { + calculationButtonServer = new StatefulButton("Calculate from server") + { + Width = setup_width, + Height = username_container_height, + Action = () => { calculateProfileFromServer(usernameTextBox.Current.Value); } + }; + + localCalcSetupContainer = new GridContainer + { + Width = setup_width, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + calculationButtonLocal = new StatefulButton("Calculate from lazer") + { + RelativeSizeAxes = Axes.X, + Height = username_container_height, + Action = () => { calculateProfileFromLazer(usernameTextBox.Current.Value); } + }, + + settingsMenu = new LazerCalculationSettings() + } + } + }; + InternalChildren = new Drawable[] { layout = new GridContainer @@ -83,14 +136,15 @@ private void load() { new Drawable[] { - new GridContainer + setupContainer = new GridContainer { - Name = "Settings", + Name = "Setup", Height = username_container_height, RelativeSizeAxes = Axes.X, ColumnDimensions = new[] { new Dimension(), + new Dimension(GridSizeMode.AutoSize), new Dimension(GridSizeMode.AutoSize) }, RowDimensions = new[] @@ -109,12 +163,12 @@ private void load() PlaceholderText = "peppy", CommitOnFocusLoss = false }, - calculationButton = new StatefulButton("Start calculation") + profileImportTypeSwitch = new SwitchButton { - Width = 150, - Height = username_container_height, - Action = () => { calculateProfile(usernameTextBox.Current.Value); } - } + Width = 80, + Height = username_container_height + }, + calculationButtonServer } } }, @@ -148,13 +202,61 @@ private void load() } }; - usernameTextBox.OnCommit += (_, _) => { calculateProfile(usernameTextBox.Current.Value); }; + profileImportTypeSwitch.Current.BindValueChanged(val => + { + calculationCancellatonToken?.Cancel(); + + if (val.NewValue) + { + setupContainer.ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize) + }; + setupContainer.Content = new[] + { + new Drawable[] + { + usernameTextBox, + profileImportTypeSwitch, + localCalcSetupContainer + } + }; + } + else + { + setupContainer.ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize) + }; + setupContainer.Content = new[] + { + new Drawable[] + { + usernameTextBox, + profileImportTypeSwitch, + calculationButtonServer + } + }; + } + }); - if (RuntimeInfo.IsDesktop) - HotReloadCallbackReceiver.CompilationFinished += _ => Schedule(() => { calculateProfile(currentUser); }); + usernameTextBox.OnCommit += (_, _) => { calculateProfile(usernameTextBox.Current.Value); }; } private void calculateProfile(string username) + { + if (profileImportTypeSwitch.Current.Value) + calculateProfileFromLazer(username); + else + calculateProfileFromServer(username); + } + + private void calculateProfileFromServer(string username) { if (string.IsNullOrEmpty(username)) { @@ -166,7 +268,7 @@ private void calculateProfile(string username) calculationCancellatonToken?.Dispose(); loadingLayer.Show(); - calculationButton.State.Value = ButtonState.Loading; + calculationButtonServer.State.Value = ButtonState.Loading; scores.Clear(); @@ -179,7 +281,7 @@ private void calculateProfile(string username) var player = await apiManager.GetJsonFromApi($"users/{username}/{ruleset.Value.ShortName}"); - currentUser = player.Username; + currentUserNicknames = [player.Username]; Schedule(() => { @@ -197,7 +299,7 @@ private void calculateProfile(string username) if (token.IsCancellationRequested) return; - var plays = new List(); + var plays = new List(); var rulesetInstance = ruleset.Value.CreateInstance(); @@ -228,10 +330,10 @@ private void calculateProfile(string username) var perfAttributes = await performanceCalculator?.CalculateAsync(parsedScore.ScoreInfo, difficultyAttributes, token)!; score.PP = perfAttributes?.Total ?? 0.0; - var extendedScore = new ExtendedScore(score, livePp, perfAttributes); + var extendedScore = new ExtendedProfileScore(score, livePp, perfAttributes); plays.Add(extendedScore); - Schedule(() => scores.Add(new ExtendedProfileScore(extendedScore))); + Schedule(() => scores.Add(new DrawableExtendedProfileScore(extendedScore))); } if (token.IsCancellationRequested) @@ -282,11 +384,236 @@ private void calculateProfile(string username) Schedule(() => { loadingLayer.Hide(); - calculationButton.State.Value = ButtonState.Done; + calculationButtonServer.State.Value = ButtonState.Done; + }); + }, token); + } + + private void calculateProfileFromLazer(string username) + { + if (string.IsNullOrEmpty(username)) + { + usernameTextBox.FlashColour(Color4.Red, 1); + return; + } + + calculationCancellatonToken?.Cancel(); + calculationCancellatonToken?.Dispose(); + + loadingLayer.Show(); + calculationButtonLocal.State.Value = ButtonState.Loading; + + scores.Clear(); + + calculationCancellatonToken = new CancellationTokenSource(); + var token = calculationCancellatonToken.Token; + + Task.Run(async () => + { + Schedule(() => loadingLayer.Text.Value = "Getting user data..."); + + APIUser player = null; + + try + { + player = await apiManager.GetJsonFromApi($"users/{username}/{ruleset.Value.ShortName}"); + + currentUserNicknames = [player.Username, .. player.PreviousUsernames, player.Id.ToString()]; + + } catch { + notificationDisplay.Display(new Notification("Unable to find player on the servers, using local name...")); + + player = new APIUser + { + Username = username + }; + + currentUserNicknames = [username]; + } + + Schedule(() => + { + if (userPanel != null) + userPanelContainer.Remove(userPanel, true); + + userPanelContainer.Add(userPanel = new UserCard(player) + { + RelativeSizeAxes = Axes.X + }); + + layout.RowDimensions = new[] { new Dimension(GridSizeMode.Absolute, username_container_height), new Dimension(GridSizeMode.AutoSize), new Dimension() }; + }); + + if (token.IsCancellationRequested) + return; + var plays = new List(); + var rulesetInstance = ruleset.Value.CreateInstance(); + var lazerPath = configManager.GetBindable(Settings.LazerFolderPath).Value; + + if (lazerPath == string.Empty) + { + notificationDisplay.Display(new Notification("Please set-up path to lazer database folder in GUI settings")); + return; + } + + var storage = gameHost.GetStorage(lazerPath); + + var realmAccess = new RealmAccess(storage, @"client.realm"); + + var realmScores = getRealmScores(realmAccess); + + int currentScoresCount = 0; + var totalScoresCount = realmScores.Sum(childList => childList.Count); + + var relevantMods = RulesetHelper.GetDifficultyAdjustingModsHashSet(rulesetInstance); + + foreach (var scoreList in realmScores) + { + string beatmapHash = scoreList[0].BeatmapHash; + + //get the .osu file from lazer file storage + var working = new FlatWorkingBeatmap(Path.Combine(lazerPath, "files", beatmapHash[..1], beatmapHash[..2], beatmapHash)); + + var difficultyCalculator = rulesetInstance.CreateDifficultyCalculator(working); + var performanceCalculator = rulesetInstance.CreatePerformanceCalculator(); + + List tempScores = []; + + Dictionary attributesCache = new Dictionary(); + + foreach (var score in scoreList) + { + if (token.IsCancellationRequested) + return; + + Schedule(() => loadingLayer.Text.Value = $"Calculating {username}'s scores... {currentScoresCount} / {totalScoresCount}"); + + if (score.BeatmapInfo == null) + continue; + + int modsHash = generateModsHash(score.Mods, relevantMods); + + if (!attributesCache.TryGetValue(modsHash, out var difficultyAttributes)) + { + difficultyAttributes = difficultyCalculator.Calculate(score.Mods); + attributesCache[modsHash] = difficultyAttributes; + } + + var perfAttributes = await performanceCalculator?.CalculateAsync(score, difficultyAttributes, token)!; + + score.PP = perfAttributes?.Total ?? 0.0; + + currentScoresCount++; + + tempScores.Add(new ProfileScore(score, perfAttributes)); + } + + var topScore = tempScores.MaxBy(s => s.SoloScore.PP); + if (topScore == null) + continue; + + plays.Add(topScore); + Schedule(() => scores.Add(new DrawableProfileScore(topScore))); + } + + if (token.IsCancellationRequested) + return; + + var localOrdered = plays.OrderByDescending(x => x.SoloScore.PP).ToList(); + + Schedule(() => + { + foreach (var play in plays) + { + play.Position.Value = localOrdered.IndexOf(play) + 1; + scores.SetLayoutPosition(scores[plays.IndexOf(play)], localOrdered.IndexOf(play)); + } + }); + + decimal totalLocalPP = 0; + for (var 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; + + //Calculate bonusPP based of unique score count on ranked diffs + var playcountBonusPP = (decimal)((417.0 - 1.0 / 3.0) * (1 - Math.Pow(0.995, Math.Min(realmScores.Count, 1000)))); + totalLocalPP += playcountBonusPP; + + Schedule(() => + { + userPanel.Data.Value = new UserCardData + { + LivePP = totalLivePP, + LocalPP = totalLocalPP, + PlaycountPP = playcountBonusPP + }; + }); + }, token).ContinueWith(t => + { + Logger.Log(t.Exception?.ToString(), level: LogLevel.Error); + notificationDisplay.Display(new Notification(t.Exception?.Flatten().Message)); + }, TaskContinuationOptions.OnlyOnFaulted).ContinueWith(t => + { + Schedule(() => + { + loadingLayer.Hide(); + calculationButtonLocal.State.Value = ButtonState.Done; }); }, token); } + private int generateModsHash(IReadOnlyList mods, HashSet relevantMods) + { + HashCode hash = new HashCode(); + + var filteredMods = mods.Where(m => relevantMods.Contains(m.GetType())); + + foreach (var mod in filteredMods) + hash.Add(mod); + + return hash.ToHashCode(); + } + + private List> getRealmScores(RealmAccess realm) + { + Schedule(() => loadingLayer.Text.Value = "Extracting user scores..."); + var realmScores = realm.Run(r => r.All().Detach()); + + Schedule(() => loadingLayer.Text.Value = "Filtering scores..."); + + realmScores.RemoveAll(x => !currentUserNicknames.Any(nickname => nickname.Equals(x.User.Username, StringComparison.OrdinalIgnoreCase)) // Wrong username + || x.BeatmapInfo == null // No map for score + || x.Passed == false || x.Rank == ScoreRank.F // Failed score + || x.Ruleset.OnlineID != ruleset.Value.OnlineID // Incorrect ruleset + || settingsMenu.ShouldBeFiltered(x)); // Customisable filters + + List> groupedScores = realmScores.GroupBy(g => g.BeatmapHash).Select(s => s.ToList()).ToList(); + + // Simulate scorev1 if enabled + if (settingsMenu.IsScorev1OverwritingEnabled) + { + var rulesetInstance = ruleset.Value.CreateInstance(); + + List> filteredScores = new List>(); + + foreach (var mapScores in groupedScores) + { + List filteredMapScores = mapScores.Where(x => x.IsLegacyScore) + .GroupBy(x => rulesetInstance.ConvertToLegacyMods(x.Mods)) + .Select(x => x.MaxBy(x => x.LegacyTotalScore)) + .ToList(); + + filteredMapScores.AddRange(mapScores.Where(s => !s.IsLegacyScore)); + filteredScores.Add(filteredMapScores); + } + + groupedScores = filteredScores; + } + + return groupedScores; + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing);