Skip to content

Commit

Permalink
Merge pull request #195 from smoogipoo/new-scores-command
Browse files Browse the repository at this point in the history
Add support for computing performance of non-legacy scores
  • Loading branch information
peppy authored Feb 11, 2024
2 parents 11e674b + 408b127 commit bfe1d34
Show file tree
Hide file tree
Showing 10 changed files with 190 additions and 68 deletions.
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)
{
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

0 comments on commit bfe1d34

Please sign in to comment.