1
0
Fork 0
mirror of https://github.com/ppy/osu-tools.git synced 2025-06-07 23:07:01 +09:00

Merge branch 'master' into fix_simulate_mania

This commit is contained in:
StanR 2025-06-06 11:59:20 +03:00 committed by GitHub
commit 70d5baa375
Signed by: github
GPG key ID: B5690EEEBB952194
13 changed files with 529 additions and 133 deletions

View file

@ -47,6 +47,7 @@ namespace PerformanceCalculator.Difficulty
mod.RequiresConfiguration,
mod.UserPlayable,
mod.ValidForMultiplayer,
mod.ValidForFreestyleAsRequiredMod,
mod.ValidForMultiplayerAsFreeMod,
mod.AlwaysValidForSubmission,
});

View file

@ -8,10 +8,10 @@
<ItemGroup>
<PackageReference Include="Alba.CsConsoleFormat" Version="1.0.0" />
<PackageReference Include="McMaster.Extensions.CommandLineUtils" Version="4.1.1" />
<PackageReference Include="ppy.osu.Game" Version="2025.227.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Osu" Version="2025.227.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Taiko" Version="2025.227.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Catch" Version="2025.227.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Mania" Version="2025.227.0" />
<PackageReference Include="ppy.osu.Game" Version="2025.530.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Osu" Version="2025.530.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Taiko" Version="2025.530.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Catch" Version="2025.530.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Mania" Version="2025.530.0" />
</ItemGroup>
</Project>

View file

@ -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<UserPlayInfo>();
@ -39,22 +43,28 @@ namespace PerformanceCalculator.Profile
Console.WriteLine("Getting user top scores...");
foreach (var play in GetJsonFromApi<List<SoloScoreInfo>>($"users/{userData.Id}/scores/best?mode={rulesetApiName}&limit=100"))
var apiScores = new List<SoloScoreInfo>();
for (int i = 0; i < max_api_scores; i += max_api_scores_in_one_query)
{
apiScores.AddRange(GetJsonFromApi<List<SoloScoreInfo>>($"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,

View file

@ -40,6 +40,10 @@ namespace PerformanceCalculator.Simulate
[Option(Template = "-X|--misses <misses>", Description = "Number of misses. Defaults to 0.")]
public int Misses { get; }
[UsedImplicitly]
[Option(Template = "-l|--legacy-total-score <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
};

View file

@ -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<ProcessorWorkingBeatmap>
{
private readonly ProcessorWorkingBeatmap beatmap;
@ -40,6 +45,10 @@ namespace PerformanceCalculatorGUI.Components
[Resolved]
private Bindable<IReadOnlyList<Mod>> mods { get; set; }
public ITooltip<ProcessorWorkingBeatmap> 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<ProcessorWorkingBeatmap>
{
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<IReadOnlyList<Mod>> mods { get; set; }
private readonly OverlayColourProvider colourProvider;
private ModSettingChangeTracker modSettingChangeTracker;
[Resolved]
private IBindable<RulesetInfo> 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<IApplicableToDifficulty>())
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 },
}
}
};
}
}
}
}

View file

@ -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

View file

@ -0,0 +1,42 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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;
}
}
}

View file

@ -6,10 +6,10 @@
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game" Version="2025.227.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Osu" Version="2025.227.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Taiko" Version="2025.227.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Catch" Version="2025.227.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Mania" Version="2025.227.0" />
<PackageReference Include="ppy.osu.Game" Version="2025.530.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Osu" Version="2025.530.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Taiko" Version="2025.530.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Catch" Version="2025.530.0" />
<PackageReference Include="ppy.osu.Game.Rulesets.Mania" Version="2025.530.0" />
</ItemGroup>
</Project>

View file

@ -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;

View file

@ -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,

View file

@ -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<string>();
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<APIUser>($"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<ExtendedScore>();
var players = new List<APIUser>();
var rulesetInstance = ruleset.Value.CreateInstance();
Schedule(() => loadingLayer.Text.Value = $"Calculating {player.Username} top scores...");
var apiScores = await apiManager.GetJsonFromApi<List<SoloScoreInfo>>($"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<List<SoloScoreInfo>>($"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<APIUser>($"users/{username}/{ruleset.Value.ShortName}").ConfigureAwait(false);
players.Add(player);
var working = ProcessorWorkingBeatmap.FromFileOrId(score.BeatmapID.ToString(), cachePath: configManager.GetBindable<string>(Settings.CachePath).Value);
Schedule(() => loadingLayer.Text.Value = $"Calculating {player.Username} top scores...");
Schedule(() => loadingLayer.Text.Value = $"Calculating {working.Metadata}");
var apiScores = new List<SoloScoreInfo>();
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<List<SoloScoreInfo>>($"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<List<SoloScoreInfo>>($"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<string>(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<ExtendedScore>();
// List of all beatmap IDs in plays without duplicates
var beatmapIDs = plays.Select(x => x.SoloScore.BeatmapID).Distinct().ToList();
foreach (int id in beatmapIDs)
{
var bestPlayOnBeatmap = plays.Where(x => x.SoloScore.BeatmapID == id).OrderByDescending(x => x.SoloScore.PP).First();
filteredPlays.Add(bestPlayOnBeatmap);
}
plays = filteredPlays;
}
var localOrdered = plays.OrderByDescending(x => x.SoloScore.PP).ToList();
var liveOrdered = plays.OrderByDescending(x => x.LivePP ?? 0).ToList();
@ -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);

View file

@ -0,0 +1,96 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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<Dictionary<string, object>> Attributes = new Bindable<Dictionary<string, object>>();
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<Dictionary<string, object>> 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();
}
}

View file

@ -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> difficultyCalculator = new Bindable<DifficultyCalculator>();
@ -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;