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:
commit
bfe1d342a7
10 changed files with 193 additions and 71 deletions
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue