// 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 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.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.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Scoring.Legacy; using osuTK.Graphics; using PerformanceCalculatorGUI.Components; using PerformanceCalculatorGUI.Configuration; namespace PerformanceCalculatorGUI.Screens { internal class ProfileScreen : PerformanceCalculatorScreen { [Cached] private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum); private StatefulButton calculationButton; private VerboseLoadingLayer loadingLayer; private GridContainer layout; private FillFlowContainer scores; private LabelledTextBox usernameTextBox; private Container userPanelContainer; private UserPPListPanel userPanel; private string currentUser; [Resolved] private APIManager apiManager { get; set; } [Resolved] private Bindable ruleset { get; set; } [Resolved] private SettingsManager configManager { get; set; } public override bool ShouldShowConfirmationDialogOnSwitch => false; private const float username_container_height = 40; public ProfileScreen() { RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] private void load() { InternalChildren = new Drawable[] { layout = new GridContainer { RelativeSizeAxes = Axes.Both, ColumnDimensions = new[] { new Dimension() }, RowDimensions = new[] { new Dimension(GridSizeMode.Absolute, username_container_height), new Dimension(GridSizeMode.Absolute), new Dimension() }, Content = new[] { new Drawable[] { new GridContainer { Name = "Settings", Height = username_container_height, RelativeSizeAxes = Axes.X, ColumnDimensions = new[] { new Dimension(), new Dimension(GridSizeMode.AutoSize) }, RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, Content = new[] { new Drawable[] { usernameTextBox = new ExtendedLabelledTextBox { RelativeSizeAxes = Axes.X, Anchor = Anchor.TopLeft, Label = "Username", PlaceholderText = "peppy", CommitOnFocusLoss = false }, calculationButton = new StatefulButton("Start calculation") { Width = 150, Height = username_container_height, Action = () => { calculateProfile(usernameTextBox.Current.Value); } } } } }, }, new Drawable[] { userPanelContainer = new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y } }, new Drawable[] { new OsuScrollContainer(Direction.Vertical) { RelativeSizeAxes = Axes.Both, Child = scores = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical } } }, } }, loadingLayer = new VerboseLoadingLayer(true) { RelativeSizeAxes = Axes.Both } }; usernameTextBox.OnCommit += (_, _) => { calculateProfile(usernameTextBox.Current.Value); }; if (RuntimeInfo.IsDesktop) HotReloadCallbackReceiver.CompilationFinished += _ => Schedule(() => { calculateProfile(currentUser); }); } private void calculateProfile(string username) { if (string.IsNullOrEmpty(username)) { usernameTextBox.FlashColour(Color4.Red, 1); return; } loadingLayer.Show(); calculationButton.State.Value = ButtonState.Loading; scores.Clear(); Task.Run(async () => { Schedule(() => loadingLayer.Text.Value = "Getting user data..."); var player = await apiManager.GetJsonFromApi($"users/{username}/{ruleset.Value.ShortName}"); currentUser = player.Username; Schedule(() => { if (userPanel != null) userPanelContainer.Remove(userPanel); userPanelContainer.Add(userPanel = new UserPPListPanel(player) { RelativeSizeAxes = Axes.X }); layout.RowDimensions = new[] { new Dimension(GridSizeMode.Absolute, username_container_height), new Dimension(GridSizeMode.AutoSize), new Dimension() }; }); var plays = 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"); foreach (var score in apiScores) { var working = ProcessorWorkingBeatmap.FromFileOrId(score.Beatmap?.OnlineID.ToString(), cachePath: configManager.GetBindable(Settings.CachePath).Value); Schedule(() => loadingLayer.Text.Value = $"Calculating {working.Metadata}"); var modsAcronyms = score.Mods.Select(x => x.ToString()).ToArray(); Mod[] mods = rulesetInstance.CreateAllMods().Where(m => modsAcronyms.Contains(m.Acronym)).ToArray(); var scoreInfo = new ScoreInfo(working.BeatmapInfo, ruleset.Value) { TotalScore = score.TotalScore, MaxCombo = score.MaxCombo, Mods = mods, Statistics = new Dictionary() }; scoreInfo.SetCount300(score.Statistics["count_300"]); scoreInfo.SetCountGeki(score.Statistics["count_geki"]); scoreInfo.SetCount100(score.Statistics["count_100"]); scoreInfo.SetCountKatu(score.Statistics["count_katu"]); scoreInfo.SetCount50(score.Statistics["count_50"]); scoreInfo.SetCountMiss(score.Statistics["count_miss"]); var parsedScore = new ProcessorScoreDecoder(working).Parse(scoreInfo); var difficultyCalculator = rulesetInstance.CreateDifficultyCalculator(working); var difficultyAttributes = difficultyCalculator.Calculate(RulesetHelper.ConvertToLegacyDifficultyAdjustmentMods(rulesetInstance, mods)); var performanceCalculator = rulesetInstance.CreatePerformanceCalculator(); var livePp = score.PP ?? 0.0; var perfAttributes = performanceCalculator?.Calculate(parsedScore.ScoreInfo, difficultyAttributes); score.PP = perfAttributes?.Total ?? 0.0; var extendedScore = new ExtendedScore(score, livePp, perfAttributes); plays.Add(extendedScore); Schedule(() => scores.Add(new ExtendedProfileScore(extendedScore))); } var localOrdered = plays.OrderByDescending(x => x.PP).ToList(); var liveOrdered = plays.OrderByDescending(x => x.LivePP).ToList(); Schedule(() => { foreach (var play in plays) { play.PositionChange.Value = liveOrdered.IndexOf(play) - localOrdered.IndexOf(play); scores.SetLayoutPosition(scores[liveOrdered.IndexOf(play)], localOrdered.IndexOf(play)); } }); int index = 0; decimal totalLocalPP = (decimal)localOrdered.Select(x=> x.PP).Sum(play => Math.Pow(0.95, index++) * play); decimal totalLivePP = player.Statistics.PP ?? (decimal)0.0; index = 0; decimal nonBonusLivePP = (decimal)liveOrdered.Select(x => x.LivePP).Sum(play => Math.Pow(0.95, index++) * play); //todo: implement properly. this is pretty damn wrong. var playcountBonusPP = (totalLivePP - nonBonusLivePP); totalLocalPP += playcountBonusPP; Schedule(() => { userPanel.Data.Value = new UserPPListPanelData { LivePP = totalLivePP, LocalPP = totalLocalPP, PlaycountPP = playcountBonusPP }; }); }).ContinueWith(t => { Schedule(() => { loadingLayer.Hide(); calculationButton.State.Value = ButtonState.Done; }); }); } } }