// Copyright (c) ppy Pty Ltd . 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.ComponentModel.DataAnnotations; using System.Globalization; using System.Linq; using JetBrains.Annotations; using McMaster.Extensions.CommandLineUtils; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; namespace PerformanceCalculator.Simulate { [Command(Name = "simulate osu", Description = "Computes the performance (pp) of a simulated osu! play.")] public class OsuSimulateCommand : SimulateCommand { [UsedImplicitly] [Required, FileExists] [Argument(0, Name = "beatmap", Description = "Required. The beatmap file (.osu).")] public override string Beatmap { get; } [UsedImplicitly] [Option(Template = "-a|--accuracy ", Description = "Accuracy. Enter as decimal 0-100. Defaults to 100." + " Scales hit results as well and is rounded to the nearest possible value for the beatmap.")] public override double Accuracy { get; } = 100; [UsedImplicitly] [Option(Template = "-c|--combo ", Description = "Maximum combo during play. Defaults to beatmap maximum.")] public override int? Combo { get; } [UsedImplicitly] [Option(Template = "-C|--percent-combo ", Description = "Percentage of beatmap maximum combo achieved. Alternative to combo option." + " Enter as decimal 0-100.")] public override double PercentCombo { get; } = 100; [UsedImplicitly] [Option(CommandOptionType.MultipleValue, Template = "-m|--mod ", Description = "One for each mod. The mods to compute the performance with." + " Values: hr, dt, hd, fl, ez, etc...")] public override string[] Mods { get; } [UsedImplicitly] [Option(Template = "-X|--misses ", Description = "Number of misses. Defaults to 0.")] public override int Misses { get; } [UsedImplicitly] [Option(Template = "-M|--mehs ", Description = "Number of mehs. Will override accuracy if used. Otherwise is automatically calculated.")] public override int? Mehs { get; } [UsedImplicitly] [Option(Template = "-G|--goods ", Description = "Number of goods. Will override accuracy if used. Otherwise is automatically calculated.")] public override int? Goods { get; } public override Ruleset Ruleset => new OsuRuleset(); protected override int GetMaxCombo(IBeatmap beatmap) => beatmap.HitObjects.Count + beatmap.HitObjects.OfType().Sum(s => s.NestedHitObjects.Count - 1); protected override Dictionary GenerateHitResults(double accuracy, IBeatmap beatmap, int countMiss, int? countMeh, int? countGood) { int countGreat; var totalResultCount = beatmap.HitObjects.Count; if (countMeh != null || countGood != null) { countGreat = totalResultCount - (countGood ?? 0) - (countMeh ?? 0) - countMiss; } else { // Let Great=6, Good=2, Meh=1, Miss=0. The total should be this. var targetTotal = (int)Math.Round(accuracy * totalResultCount * 6); // Start by assuming every non miss is a meh // This is how much increase is needed by greats and goods var delta = targetTotal - (totalResultCount - countMiss); // Each great increases total by 5 (great-meh=5) countGreat = delta / 5; // Each good increases total by 1 (good-meh=1). Covers remaining difference. countGood = delta % 5; // Mehs are left over. Could be negative if impossible value of amountMiss chosen countMeh = totalResultCount - countGreat - countGood - countMiss; } return new Dictionary { { HitResult.Great, countGreat }, { HitResult.Good, countGood ?? 0 }, { HitResult.Meh, countMeh ?? 0 }, { HitResult.Miss, countMiss } }; } protected override double GetAccuracy(Dictionary statistics) { var countGreat = statistics[HitResult.Great]; var countGood = statistics[HitResult.Good]; var countMeh = statistics[HitResult.Meh]; var countMiss = statistics[HitResult.Miss]; var total = countGreat + countGood + countMeh + countMiss; return (double)((6 * countGreat) + (2 * countGood) + countMeh) / (6 * total); } protected override void WritePlayInfo(ScoreInfo scoreInfo, IBeatmap beatmap) { WriteAttribute("Accuracy", (scoreInfo.Accuracy * 100).ToString(CultureInfo.InvariantCulture) + "%"); WriteAttribute("Combo", FormattableString.Invariant($"{scoreInfo.MaxCombo} ({Math.Round(100.0 * scoreInfo.MaxCombo / GetMaxCombo(beatmap), 2)}%)")); foreach (var statistic in scoreInfo.Statistics) { WriteAttribute(Enum.GetName(typeof(HitResult), statistic.Key), statistic.Value.ToString(CultureInfo.InvariantCulture)); } } } }