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

Merge pull request #195 from smoogipoo/new-scores-command

Add support for computing performance of non-legacy scores
This commit is contained in:
Dean Herbert 2024-02-11 15:59:54 +08:00 committed by GitHub
commit bfe1d342a7
Signed by: github
GPG key ID: B5690EEEBB952194
10 changed files with 193 additions and 71 deletions

View file

@ -1,6 +1,7 @@
// 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.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Net.Http;
@ -33,11 +34,19 @@ namespace PerformanceCalculator
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;

View file

@ -124,7 +124,7 @@ namespace PerformanceCalculator.Difficulty
{
// 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

View file

@ -62,7 +62,7 @@ namespace PerformanceCalculator.Leaderboard
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));

View file

@ -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
{
@ -63,51 +63,73 @@ namespace PerformanceCalculator
}
}
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)
{
LegacyMods relevantMods = LegacyMods.DoubleTime | LegacyMods.HalfTime | LegacyMods.HardRock | LegacyMods.Easy;
switch (rulesetId)
{
case 0:
if ((mods & LegacyMods.Flashlight) > 0)
relevantMods |= LegacyMods.Flashlight | LegacyMods.Hidden | LegacyMods.TouchDevice;
else
relevantMods |= LegacyMods.Flashlight | LegacyMods.TouchDevice;
break;
case 3:
if (isConvertedBeatmap)
relevantMods |= KEY_MODS;
break;
}
return mods & relevantMods;
}
/// <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 static LegacyMods ConvertToLegacyDifficultyAdjustmentMods(BeatmapInfo beatmapInfo, Ruleset ruleset, Mod[] mods)
{
var allMods = ruleset.CreateAllMods().ToArray();
var legacyMods = ruleset.ConvertToLegacyMods(mods);
var allowedMods = ModUtils.FlattenMods(
ruleset.CreateDifficultyCalculator(new EmptyWorkingBeatmap(beatmapInfo)).CreateDifficultyAdjustmentModCombinations())
.Select(m => m.GetType())
.Distinct()
.ToHashSet();
// 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;
// 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());
var result = new List<Mod>();
var classicMod = allMods.SingleOrDefault(m => m is ModClassic);
if (classicMod != null)
result.Add(classicMod);
result.AddRange(mods.Where(m => allowedMods.Contains(m.GetType())));
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)
{
case 0:
return new OsuDifficultyAttributes();
case 1:
return new TaikoDifficultyAttributes();
case 2:
return new CatchDifficultyAttributes();
case 3:
return new ManiaDifficultyAttributes();
default:
throw new ArgumentException($"Invalid ruleset ID: {legacyId}", nameof(legacyId));
}
protected override IBeatmap GetBeatmap() => throw new NotImplementedException();
public override Texture GetBackground() => throw new NotImplementedException();
protected override Track GetBeatmapTrack() => throw new NotImplementedException();
protected override ISkin GetSkin() => throw new NotImplementedException();
public override Stream GetStream(string storagePath) => throw new NotImplementedException();
}
}
}

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

View file

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

View file

@ -42,7 +42,7 @@ namespace PerformanceCalculator.Performance
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(

View file

@ -1,36 +1,70 @@
// 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.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
@ -40,27 +74,41 @@ namespace PerformanceCalculator.Performance
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) }
};
switch (rulesetId)
{
case 0:
return GetJsonFromApi<AttributesResponse<OsuDifficultyAttributes>>($"beatmaps/{beatmapId}/attributes", HttpMethod.Post, parameters).Attributes;
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));
}
}
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);
OutputPerformance(score, performanceAttributes, difficultyAttributes);
[JsonObject(MemberSerialization.OptIn)]
private class AttributesResponse<T>
where T : DifficultyAttributes
{
[JsonProperty("attributes")]
public T Attributes { get; set; }
}
}
}

View file

@ -52,7 +52,7 @@ namespace PerformanceCalculator.Profile
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);

View file

@ -57,7 +57,7 @@ namespace PerformanceCalculator.Simulate
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);