Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for computing performance of non-legacy scores #195

Merged
merged 7 commits into from
Feb 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion PerformanceCalculator/ApiCommand.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Net.Http;
Expand Down Expand Up @@ -33,11 +34,19 @@ public override void OnExecute(CommandLineApplication app, IConsole console)
base.OnExecute(app, console);
}

protected T GetJsonFromApi<T>(string request)
protected T GetJsonFromApi<T>(string request, HttpMethod method = null, Dictionary<string, string> parameters = null)
{
using var req = new JsonWebRequest<T>($"{Program.ENDPOINT_CONFIGURATION.APIEndpointUrl}/api/v2/{request}");
req.Method = method ?? HttpMethod.Get;
req.AddHeader("x-api-version", api_version.ToString(CultureInfo.InvariantCulture));
req.AddHeader(System.Net.HttpRequestHeader.Authorization.ToString(), $"Bearer {apiAccessToken}");

if (parameters != null)
{
foreach ((string key, string value) in parameters)
req.AddParameter(key, value);
}

req.Perform();

return req.ResponseObject;
Expand Down
2 changes: 1 addition & 1 deletion PerformanceCalculator/Difficulty/DifficultyCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ private Result processBeatmap(WorkingBeatmap beatmap)
{
// Get the ruleset
var ruleset = LegacyHelper.GetRulesetFromLegacyID(Ruleset ?? beatmap.BeatmapInfo.Ruleset.OnlineID);
var mods = NoClassicMod ? getMods(ruleset) : LegacyHelper.ConvertToLegacyDifficultyAdjustmentMods(beatmap.BeatmapInfo, ruleset, getMods(ruleset));
var mods = NoClassicMod ? getMods(ruleset) : LegacyHelper.FilterDifficultyAdjustmentMods(beatmap.BeatmapInfo, ruleset, getMods(ruleset));
var attributes = ruleset.CreateDifficultyCalculator(beatmap).Calculate(mods);

return new Result
Expand Down
2 changes: 1 addition & 1 deletion PerformanceCalculator/Leaderboard/LeaderboardCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public override void Execute()
var score = new ProcessorScoreDecoder(working).Parse(scoreInfo);

var difficultyCalculator = ruleset.CreateDifficultyCalculator(working);
var difficultyAttributes = difficultyCalculator.Calculate(LegacyHelper.ConvertToLegacyDifficultyAdjustmentMods(working.BeatmapInfo, ruleset, scoreInfo.Mods).ToArray());
var difficultyAttributes = difficultyCalculator.Calculate(LegacyHelper.FilterDifficultyAdjustmentMods(working.BeatmapInfo, ruleset, scoreInfo.Mods).ToArray());
var performanceCalculator = ruleset.CreatePerformanceCalculator();

plays.Add((performanceCalculator?.Calculate(score.ScoreInfo, difficultyAttributes).Total ?? 0, play.PP ?? 0.0));
Expand Down
94 changes: 58 additions & 36 deletions PerformanceCalculator/LegacyHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Catch.Difficulty;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mania.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Difficulty;
using osu.Game.Rulesets.Taiko;
using osu.Game.Skinning;
using osu.Game.Utils;
using osu.Game.Rulesets.Taiko.Difficulty;

namespace PerformanceCalculator
{
Expand Down Expand Up @@ -63,51 +63,73 @@ public static string GetRulesetShortNameFromId(int id)
}
}

/// <summary>
/// Transforms a given <see cref="Mod"/> 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.
/// </summary>
public static Mod[] ConvertToLegacyDifficultyAdjustmentMods(BeatmapInfo beatmapInfo, Ruleset ruleset, Mod[] mods)
public const LegacyMods KEY_MODS = LegacyMods.Key1 | LegacyMods.Key2 | LegacyMods.Key3 | LegacyMods.Key4 | LegacyMods.Key5 | LegacyMods.Key6 | LegacyMods.Key7 | LegacyMods.Key8
| LegacyMods.Key9 | LegacyMods.KeyCoop;

// See: https://github.com/ppy/osu-queue-score-statistics/blob/2264bfa68e14bb16ec71a7cac2072bdcfaf565b6/osu.Server.Queues.ScoreStatisticsProcessor/Helpers/LegacyModsHelper.cs
public static LegacyMods MaskRelevantMods(LegacyMods mods, bool isConvertedBeatmap, int rulesetId)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could at least link back to the osu-queue-score-statistics version of this.

{
var allMods = ruleset.CreateAllMods().ToArray();
LegacyMods relevantMods = LegacyMods.DoubleTime | LegacyMods.HalfTime | LegacyMods.HardRock | LegacyMods.Easy;

var allowedMods = ModUtils.FlattenMods(
ruleset.CreateDifficultyCalculator(new EmptyWorkingBeatmap(beatmapInfo)).CreateDifficultyAdjustmentModCombinations())
.Select(m => m.GetType())
.Distinct()
.ToHashSet();
switch (rulesetId)
{
case 0:
if ((mods & LegacyMods.Flashlight) > 0)
relevantMods |= LegacyMods.Flashlight | LegacyMods.Hidden | LegacyMods.TouchDevice;
else
relevantMods |= LegacyMods.Flashlight | LegacyMods.TouchDevice;
break;

// 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());
case 3:
if (isConvertedBeatmap)
relevantMods |= KEY_MODS;
break;
}

var result = new List<Mod>();
return mods & relevantMods;
}

var classicMod = allMods.SingleOrDefault(m => m is ModClassic);
if (classicMod != null)
result.Add(classicMod);
/// <summary>
/// Transforms a given <see cref="Mod"/> 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.
/// </summary>
public static LegacyMods ConvertToLegacyDifficultyAdjustmentMods(BeatmapInfo beatmapInfo, Ruleset ruleset, Mod[] mods)
{
var legacyMods = ruleset.ConvertToLegacyMods(mods);

result.AddRange(mods.Where(m => allowedMods.Contains(m.GetType())));
// mods that are not represented in `LegacyMods` (but we can approximate them well enough with others)
if (mods.Any(mod => mod is ModDaycore))
legacyMods |= LegacyMods.HalfTime;

return result.ToArray();
return MaskRelevantMods(legacyMods, ruleset.RulesetInfo.OnlineID != beatmapInfo.Ruleset.OnlineID, ruleset.RulesetInfo.OnlineID);
}

private class EmptyWorkingBeatmap : WorkingBeatmap
/// <summary>
/// Transforms a given <see cref="Mod"/> 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.
/// </summary>
public static Mod[] FilterDifficultyAdjustmentMods(BeatmapInfo beatmapInfo, Ruleset ruleset, Mod[] mods)
=> ruleset.ConvertFromLegacyMods(ConvertToLegacyDifficultyAdjustmentMods(beatmapInfo, ruleset, mods)).ToArray();

public static DifficultyAttributes CreateDifficultyAttributes(int legacyId)
{
public EmptyWorkingBeatmap(BeatmapInfo beatmapInfo)
: base(beatmapInfo, null)
switch (legacyId)
{
}

protected override IBeatmap GetBeatmap() => throw new NotImplementedException();
case 0:
return new OsuDifficultyAttributes();

public override Texture GetBackground() => throw new NotImplementedException();
case 1:
return new TaikoDifficultyAttributes();

protected override Track GetBeatmapTrack() => throw new NotImplementedException();
case 2:
return new CatchDifficultyAttributes();

protected override ISkin GetSkin() => throw new NotImplementedException();
case 3:
return new ManiaDifficultyAttributes();

public override Stream GetStream(string storagePath) => throw new NotImplementedException();
default:
throw new ArgumentException($"Invalid ruleset ID: {legacyId}", nameof(legacyId));
}
}
}
}
42 changes: 42 additions & 0 deletions PerformanceCalculator/Performance/LegacyScorePerformanceCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System.Linq;
using McMaster.Extensions.CommandLineUtils;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring.Legacy;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;

namespace PerformanceCalculator.Performance
{
[Command(Name = "legacy-score", Description = "Computes the performance (pp) of an online score.")]
public class LegacyScorePerformanceCommand : ScorePerformanceCommand
{
[Argument(1, "ruleset-id", "The ID of the ruleset that the score was set on.")]
public int RulesetId { get; set; }

protected override SoloScoreInfo QueryScore() => GetJsonFromApi<SoloScoreInfo>($"scores/{LegacyHelper.GetRulesetShortNameFromId(RulesetId)}/{ScoreId}");

protected override ScoreInfo CreateScore(SoloScoreInfo apiScore, Ruleset ruleset, APIBeatmap apiBeatmap, WorkingBeatmap workingBeatmap)
{
var score = base.CreateScore(apiScore, ruleset, apiBeatmap, workingBeatmap);

score.Mods = score.Mods.Append(ruleset.CreateMod<ModClassic>()).ToArray();
score.IsLegacyScore = true;
score.LegacyTotalScore = (int)score.TotalScore;
LegacyScoreDecoder.PopulateMaximumStatistics(score, workingBeatmap);
StandardisedScoreMigrationTools.UpdateFromLegacy(
score,
ruleset,
LegacyBeatmapConversionDifficultyInfo.FromAPIBeatmap(apiBeatmap),
((ILegacyRuleset)ruleset).CreateLegacyScoreSimulator().Simulate(workingBeatmap, workingBeatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods)));

return score;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace PerformanceCalculator.Performance
[Command(Name = "performance", Description = "Computes the performance (pp) of scores or replays.")]
[Subcommand(typeof(ReplayPerformanceCommand))]
[Subcommand(typeof(ScorePerformanceCommand))]
[Subcommand(typeof(LegacyScorePerformanceCommand))]
public class PerformanceListingCommand
{
[UsedImplicitly]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public override void Execute()

if (score.ScoreInfo.IsLegacyScore)
{
difficultyMods = LegacyHelper.ConvertToLegacyDifficultyAdjustmentMods(workingBeatmap.BeatmapInfo, ruleset, difficultyMods);
difficultyMods = LegacyHelper.FilterDifficultyAdjustmentMods(workingBeatmap.BeatmapInfo, ruleset, difficultyMods);
score.ScoreInfo.LegacyTotalScore = (int)score.ScoreInfo.TotalScore;
LegacyScoreDecoder.PopulateMaximumStatistics(score.ScoreInfo, workingBeatmap);
StandardisedScoreMigrationTools.UpdateFromLegacy(
Expand Down
100 changes: 74 additions & 26 deletions PerformanceCalculator/Performance/ScorePerformanceCommand.cs
Original file line number Diff line number Diff line change
@@ -1,36 +1,70 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. 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.Globalization;
using System.Linq;
using System.Net.Http;
using McMaster.Extensions.CommandLineUtils;
using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Models;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring.Legacy;
using osu.Game.Scoring.Legacy;
using osu.Game.Rulesets.Catch.Difficulty;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mania.Difficulty;
using osu.Game.Rulesets.Osu.Difficulty;
using osu.Game.Rulesets.Taiko.Difficulty;
using osu.Game.Scoring;

namespace PerformanceCalculator.Performance
{
[Command(Name = "score", Description = "Computes the performance (pp) of an online score.")]
public class ScorePerformanceCommand : ApiCommand
{
[Argument(0, "ruleset-id", "The ID of the ruleset that the score was set on.")]
public int RulesetId { get; set; }

[Argument(1, "score-id", "The score's online ID.")]
[Argument(0, "score-id", "The score's online ID.")]
public ulong ScoreId { get; set; }

[Option(CommandOptionType.NoValue, Template = "-a|--online-attributes", Description = "Whether to use the currently-live difficulty attributes for the beatmap.")]
public bool OnlineAttributes { get; set; }

public override void Execute()
{
base.Execute();

SoloScoreInfo apiScore = GetJsonFromApi<SoloScoreInfo>($"scores/{LegacyHelper.GetRulesetShortNameFromId(RulesetId)}/{ScoreId}");
SoloScoreInfo apiScore = QueryScore();
APIBeatmap apiBeatmap = GetJsonFromApi<APIBeatmap>($"beatmaps/lookup?id={apiScore.BeatmapID}");

var ruleset = LegacyHelper.GetRulesetFromLegacyID(apiScore.RulesetID);
var workingBeatmap = ProcessorWorkingBeatmap.FromFileOrId(apiScore.BeatmapID.ToString());
var score = CreateScore(apiScore, ruleset, apiBeatmap, workingBeatmap);

DifficultyAttributes attributes;

if (OnlineAttributes)
{
LegacyMods legacyMods = LegacyHelper.ConvertToLegacyDifficultyAdjustmentMods(workingBeatmap.BeatmapInfo, ruleset, score.Mods);
attributes = queryApiAttributes(apiScore.BeatmapID, apiScore.RulesetID, legacyMods);
}
else
{
var difficultyCalculator = ruleset.CreateDifficultyCalculator(workingBeatmap);
attributes = difficultyCalculator.Calculate(LegacyHelper.FilterDifficultyAdjustmentMods(workingBeatmap.BeatmapInfo, ruleset, score.Mods));
}

var performanceCalculator = ruleset.CreatePerformanceCalculator();
var performanceAttributes = performanceCalculator?.Calculate(score, attributes);

OutputPerformance(score, performanceAttributes, attributes);
}

protected virtual SoloScoreInfo QueryScore() => GetJsonFromApi<SoloScoreInfo>($"scores/{ScoreId}");

protected virtual ScoreInfo CreateScore(SoloScoreInfo apiScore, Ruleset ruleset, APIBeatmap apiBeatmap, WorkingBeatmap workingBeatmap)
{
var score = apiScore.ToScoreInfo(apiScore.Mods.Select(m => m.ToMod(ruleset)).ToArray(), apiBeatmap);
score.Ruleset = ruleset.RulesetInfo;
score.BeatmapInfo!.Metadata = new BeatmapMetadata
Expand All @@ -40,27 +74,41 @@ public override void Execute()
Author = new RealmUser { Username = apiBeatmap.Metadata.Author.Username },
};

var workingBeatmap = ProcessorWorkingBeatmap.FromFileOrId(score.BeatmapInfo!.OnlineID.ToString());
return score;
}

if (apiScore.BuildID == null)
private DifficultyAttributes queryApiAttributes(int beatmapId, int rulesetId, LegacyMods mods)
{
Dictionary<string, string> parameters = new Dictionary<string, string>
{
score.Mods = score.Mods.Append(ruleset.CreateMod<ModClassic>()).ToArray();
score.IsLegacyScore = true;
score.LegacyTotalScore = (int)score.TotalScore;
LegacyScoreDecoder.PopulateMaximumStatistics(score, workingBeatmap);
StandardisedScoreMigrationTools.UpdateFromLegacy(
score,
ruleset,
LegacyBeatmapConversionDifficultyInfo.FromAPIBeatmap(apiBeatmap),
((ILegacyRuleset)ruleset).CreateLegacyScoreSimulator().Simulate(workingBeatmap, workingBeatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods)));
}
{ "mods", ((int)mods).ToString(CultureInfo.InvariantCulture) }
};

var difficultyCalculator = ruleset.CreateDifficultyCalculator(workingBeatmap);
var difficultyAttributes = difficultyCalculator.Calculate(LegacyHelper.ConvertToLegacyDifficultyAdjustmentMods(workingBeatmap.BeatmapInfo, ruleset, score.Mods));
var performanceCalculator = ruleset.CreatePerformanceCalculator();
var performanceAttributes = performanceCalculator?.Calculate(score, difficultyAttributes);
switch (rulesetId)
{
case 0:
return GetJsonFromApi<AttributesResponse<OsuDifficultyAttributes>>($"beatmaps/{beatmapId}/attributes", HttpMethod.Post, parameters).Attributes;

OutputPerformance(score, performanceAttributes, difficultyAttributes);
case 1:
return GetJsonFromApi<AttributesResponse<TaikoDifficultyAttributes>>($"beatmaps/{beatmapId}/attributes", HttpMethod.Post, parameters).Attributes;

case 2:
return GetJsonFromApi<AttributesResponse<CatchDifficultyAttributes>>($"beatmaps/{beatmapId}/attributes", HttpMethod.Post, parameters).Attributes;

case 3:
return GetJsonFromApi<AttributesResponse<ManiaDifficultyAttributes>>($"beatmaps/{beatmapId}/attributes", HttpMethod.Post, parameters).Attributes;

default:
throw new ArgumentOutOfRangeException(nameof(rulesetId));
}
}

[JsonObject(MemberSerialization.OptIn)]
private class AttributesResponse<T>
where T : DifficultyAttributes
{
[JsonProperty("attributes")]
public T Attributes { get; set; }
}
}
}
2 changes: 1 addition & 1 deletion PerformanceCalculator/Profile/ProfileCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public override void Execute()
var score = new ProcessorScoreDecoder(working).Parse(scoreInfo);

var difficultyCalculator = ruleset.CreateDifficultyCalculator(working);
var difficultyAttributes = difficultyCalculator.Calculate(LegacyHelper.ConvertToLegacyDifficultyAdjustmentMods(working.BeatmapInfo, ruleset, scoreInfo.Mods).ToArray());
var difficultyAttributes = difficultyCalculator.Calculate(LegacyHelper.FilterDifficultyAdjustmentMods(working.BeatmapInfo, ruleset, scoreInfo.Mods).ToArray());
var performanceCalculator = ruleset.CreatePerformanceCalculator();

var ppAttributes = performanceCalculator?.Calculate(score.ScoreInfo, difficultyAttributes);
Expand Down
2 changes: 1 addition & 1 deletion PerformanceCalculator/Simulate/SimulateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public override void Execute()
var ruleset = Ruleset;

var workingBeatmap = ProcessorWorkingBeatmap.FromFileOrId(Beatmap);
var mods = NoClassicMod ? GetMods(ruleset) : LegacyHelper.ConvertToLegacyDifficultyAdjustmentMods(workingBeatmap.BeatmapInfo, ruleset, GetMods(ruleset));
var mods = NoClassicMod ? GetMods(ruleset) : LegacyHelper.FilterDifficultyAdjustmentMods(workingBeatmap.BeatmapInfo, ruleset, GetMods(ruleset));
var beatmap = workingBeatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods);

var beatmapMaxCombo = GetMaxCombo(beatmap);
Expand Down
Loading