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

Combine commands and processors

This commit is contained in:
Dean Herbert 2019-01-24 17:22:05 +09:00
parent c827fad168
commit 4070c06998
20 changed files with 506 additions and 653 deletions

View file

@ -1,12 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-tools/master/LICENCE
using McMaster.Extensions.CommandLineUtils;
namespace PerformanceCalculator
{
[HelpOption("-?|-h|--help")]
public abstract class CommandBase
{
}
}

View file

@ -1,9 +1,21 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-tools/master/LICENCE
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
using System.Linq;
using JetBrains.Annotations;
using McMaster.Extensions.CommandLineUtils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch.Difficulty;
using osu.Game.Rulesets.Mania.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty;
using osu.Game.Rulesets.Taiko.Difficulty;
namespace PerformanceCalculator.Difficulty
{
@ -26,6 +38,74 @@ namespace PerformanceCalculator.Difficulty
+ "Values: hr, dt, hd, fl, ez, 4k, 5k, etc...")]
public string[] Mods { get; }
protected override IProcessor CreateProcessor() => new DifficultyProcessor(this);
public override void Execute()
{
if (Directory.Exists(Path))
{
foreach (string file in Directory.GetFiles(Path, "*.osu", SearchOption.AllDirectories))
{
var beatmap = new ProcessorWorkingBeatmap(file);
Console.WriteLine(beatmap.BeatmapInfo.ToString());
processBeatmap(beatmap);
}
}
else
processBeatmap(new ProcessorWorkingBeatmap(Path));
}
private void processBeatmap(WorkingBeatmap beatmap)
{
// Get the ruleset
var ruleset = LegacyHelper.GetRulesetFromLegacyID(Ruleset ?? beatmap.BeatmapInfo.RulesetID);
var attributes = ruleset.CreateDifficultyCalculator(beatmap).Calculate(getMods(ruleset).ToArray());
writeAttribute("Ruleset", ruleset.ShortName);
writeAttribute("Stars", attributes.StarRating.ToString(CultureInfo.InvariantCulture));
switch (attributes)
{
case OsuDifficultyAttributes osu:
writeAttribute("Aim", osu.AimStrain.ToString(CultureInfo.InvariantCulture));
writeAttribute("Speed", osu.SpeedStrain.ToString(CultureInfo.InvariantCulture));
writeAttribute("MaxCombo", osu.MaxCombo.ToString(CultureInfo.InvariantCulture));
writeAttribute("AR", osu.ApproachRate.ToString(CultureInfo.InvariantCulture));
writeAttribute("OD", osu.OverallDifficulty.ToString(CultureInfo.InvariantCulture));
break;
case TaikoDifficultyAttributes taiko:
writeAttribute("HitWindow", taiko.GreatHitWindow.ToString(CultureInfo.InvariantCulture));
writeAttribute("MaxCombo", taiko.MaxCombo.ToString(CultureInfo.InvariantCulture));
break;
case CatchDifficultyAttributes c:
writeAttribute("MaxCombo", c.MaxCombo.ToString(CultureInfo.InvariantCulture));
writeAttribute("AR", c.ApproachRate.ToString(CultureInfo.InvariantCulture));
break;
case ManiaDifficultyAttributes mania:
writeAttribute("HitWindow", mania.GreatHitWindow.ToString(CultureInfo.InvariantCulture));
break;
}
Console.WriteLine();
}
private void writeAttribute(string name, string value) => Console.WriteLine($"{name.PadRight(15)}: {value}");
private List<Mod> getMods(Ruleset ruleset)
{
var mods = new List<Mod>();
if (Mods == null)
return mods;
var availableMods = ruleset.GetAllMods().ToList();
foreach (var modString in Mods)
{
Mod newMod = availableMods.FirstOrDefault(m => string.Equals(m.Acronym, modString, StringComparison.CurrentCultureIgnoreCase));
if (newMod == null)
throw new ArgumentException($"Invalid mod provided: {modString}");
mods.Add(newMod);
}
return mods;
}
}
}

View file

@ -1,99 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-tools/master/LICENCE
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using McMaster.Extensions.CommandLineUtils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch.Difficulty;
using osu.Game.Rulesets.Mania.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty;
using osu.Game.Rulesets.Taiko.Difficulty;
namespace PerformanceCalculator.Difficulty
{
public class DifficultyProcessor : IProcessor
{
private readonly DifficultyCommand command;
public DifficultyProcessor(DifficultyCommand command)
{
this.command = command;
}
public void Execute()
{
if (Directory.Exists(command.Path))
{
foreach (string file in Directory.GetFiles(command.Path, "*.osu", SearchOption.AllDirectories))
{
var beatmap = new ProcessorWorkingBeatmap(file);
command.Console.WriteLine(beatmap.BeatmapInfo.ToString());
processBeatmap(beatmap);
}
}
else
processBeatmap(new ProcessorWorkingBeatmap(command.Path));
}
private void processBeatmap(WorkingBeatmap beatmap)
{
// Get the ruleset
var ruleset = LegacyHelper.GetRulesetFromLegacyID(command.Ruleset ?? beatmap.BeatmapInfo.RulesetID);
var attributes = ruleset.CreateDifficultyCalculator(beatmap).Calculate(getMods(ruleset).ToArray());
writeAttribute("Ruleset", ruleset.ShortName);
writeAttribute("Stars", attributes.StarRating.ToString(CultureInfo.InvariantCulture));
switch (attributes)
{
case OsuDifficultyAttributes osu:
writeAttribute("Aim", osu.AimStrain.ToString(CultureInfo.InvariantCulture));
writeAttribute("Speed", osu.SpeedStrain.ToString(CultureInfo.InvariantCulture));
writeAttribute("MaxCombo", osu.MaxCombo.ToString(CultureInfo.InvariantCulture));
writeAttribute("AR", osu.ApproachRate.ToString(CultureInfo.InvariantCulture));
writeAttribute("OD", osu.OverallDifficulty.ToString(CultureInfo.InvariantCulture));
break;
case TaikoDifficultyAttributes taiko:
writeAttribute("HitWindow", taiko.GreatHitWindow.ToString(CultureInfo.InvariantCulture));
writeAttribute("MaxCombo", taiko.MaxCombo.ToString(CultureInfo.InvariantCulture));
break;
case CatchDifficultyAttributes c:
writeAttribute("MaxCombo", c.MaxCombo.ToString(CultureInfo.InvariantCulture));
writeAttribute("AR", c.ApproachRate.ToString(CultureInfo.InvariantCulture));
break;
case ManiaDifficultyAttributes mania:
writeAttribute("HitWindow", mania.GreatHitWindow.ToString(CultureInfo.InvariantCulture));
break;
}
command.Console.WriteLine();
}
private void writeAttribute(string name, string value) => command.Console.WriteLine($"{name.PadRight(15)}: {value}");
private List<Mod> getMods(Ruleset ruleset)
{
var mods = new List<Mod>();
if (command.Mods == null)
return mods;
var availableMods = ruleset.GetAllMods().ToList();
foreach (var modString in command.Mods)
{
Mod newMod = availableMods.FirstOrDefault(m => string.Equals(m.Acronym, modString, StringComparison.CurrentCultureIgnoreCase));
if (newMod == null)
throw new ArgumentException($"Invalid mod provided: {modString}");
mods.Add(newMod);
}
return mods;
}
}
}

View file

@ -1,16 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-tools/master/LICENCE
namespace PerformanceCalculator
{
/// <summary>
/// Interface for the processors of all <see cref="ProcessorCommand"/>.
/// </summary>
public interface IProcessor
{
/// <summary>
/// Processes the command.
/// </summary>
void Execute();
}
}

View file

@ -1,9 +1,14 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-tools/master/LICENCE
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
using System.Linq;
using JetBrains.Annotations;
using McMaster.Extensions.CommandLineUtils;
using osu.Game.Scoring;
namespace PerformanceCalculator.Performance
{
@ -19,7 +24,38 @@ namespace PerformanceCalculator.Performance
[FileExists]
[Option(Template = "-r|--replay <file>", Description = "One for each replay. The replay file.")]
public string[] Replays { get; }
public override void Execute()
{
var workingBeatmap = new ProcessorWorkingBeatmap(Beatmap);
var scoreParser = new ProcessorScoreParser(workingBeatmap);
protected override IProcessor CreateProcessor() => new PerformanceProcessor(this);
foreach (var f in Replays)
{
Score score;
using (var stream = File.OpenRead(f))
score = scoreParser.Parse(stream);
workingBeatmap.Mods.Value = score.ScoreInfo.Mods;
// Convert + process beatmap
var categoryAttribs = new Dictionary<string, double>();
double pp = score.ScoreInfo.Ruleset.CreateInstance().CreatePerformanceCalculator(workingBeatmap, score.ScoreInfo).Calculate(categoryAttribs);
Console.WriteLine(f);
writeAttribute("Player", score.ScoreInfo.User.Username);
writeAttribute("Mods", score.ScoreInfo.Mods.Length > 0
? score.ScoreInfo.Mods.Select(m => m.Acronym).Aggregate((c, n) => $"{c}, {n}")
: "None");
foreach (var kvp in categoryAttribs)
writeAttribute(kvp.Key, kvp.Value.ToString(CultureInfo.InvariantCulture));
writeAttribute("pp", pp.ToString(CultureInfo.InvariantCulture));
Console.WriteLine();
}
}
private void writeAttribute(string name, string value) => Console.WriteLine($"{name.PadRight(15)}: {value}");
}
}

View file

@ -1,55 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-tools/master/LICENCE
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using McMaster.Extensions.CommandLineUtils;
using osu.Game.Scoring;
namespace PerformanceCalculator.Performance
{
public class PerformanceProcessor : IProcessor
{
private readonly PerformanceCommand command;
public PerformanceProcessor(PerformanceCommand command)
{
this.command = command;
}
public void Execute()
{
var workingBeatmap = new ProcessorWorkingBeatmap(command.Beatmap);
var scoreParser = new ProcessorScoreParser(workingBeatmap);
foreach (var f in command.Replays)
{
Score score;
using (var stream = File.OpenRead(f))
score = scoreParser.Parse(stream);
workingBeatmap.Mods.Value = score.ScoreInfo.Mods;
// Convert + process beatmap
var categoryAttribs = new Dictionary<string, double>();
double pp = score.ScoreInfo.Ruleset.CreateInstance().CreatePerformanceCalculator(workingBeatmap, score.ScoreInfo).Calculate(categoryAttribs);
command.Console.WriteLine(f);
writeAttribute("Player", score.ScoreInfo.User.Username);
writeAttribute("Mods", score.ScoreInfo.Mods.Length > 0
? score.ScoreInfo.Mods.Select(m => m.Acronym).Aggregate((c, n) => $"{c}, {n}")
: "None");
foreach (var kvp in categoryAttribs)
writeAttribute(kvp.Key, kvp.Value.ToString(CultureInfo.InvariantCulture));
writeAttribute("pp", pp.ToString(CultureInfo.InvariantCulture));
command.Console.WriteLine();
}
}
private void writeAttribute(string name, string value) => command.Console.WriteLine($"{name.PadRight(15)}: {value}");
}
}

View file

@ -1,12 +1,15 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-tools/master/LICENCE
using System.IO;
using Alba.CsConsoleFormat;
using JetBrains.Annotations;
using McMaster.Extensions.CommandLineUtils;
namespace PerformanceCalculator
{
public abstract class ProcessorCommand : CommandBase
[HelpOption("-?|-h|--help")]
public abstract class ProcessorCommand
{
/// <summary>
/// The console.
@ -20,13 +23,32 @@ namespace PerformanceCalculator
public void OnExecute(CommandLineApplication app, IConsole console)
{
Console = console;
CreateProcessor().Execute();
Execute();
}
/// <summary>
/// Creates the <see cref="IProcessor"/> to process this <see cref="ProcessorCommand"/>.
/// </summary>
/// <returns>The <see cref="IProcessor"/>.</returns>
protected abstract IProcessor CreateProcessor();
public void OutputDocument(Document document)
{
// todo: make usable by other command
using (var writer = new StringWriter())
{
ConsoleRenderer.RenderDocumentToText(document, new TextRenderTarget(writer));
var str = writer.GetStringBuilder().ToString();
var lines = str.Split('\n');
for (int i = 0; i < lines.Length; i++)
lines[i] = lines[i].TrimEnd();
str = string.Join('\n', lines);
Console.Write(str);
if (OutputFile != null)
File.WriteAllText(OutputFile, str);
}
}
public virtual void Execute()
{
}
}
}

View file

@ -1,9 +1,19 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-tools/master/LICENCE
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using Alba.CsConsoleFormat;
using JetBrains.Annotations;
using McMaster.Extensions.CommandLineUtils;
using osu.Framework.IO.Network;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace PerformanceCalculator.Profile
{
@ -25,6 +35,103 @@ namespace PerformanceCalculator.Profile
[AllowedValues("0", "1", "2", "3")]
public int? Ruleset { get; }
protected override IProcessor CreateProcessor() => new ProfileProcessor(this);
private const string base_url = "https://osu.ppy.sh";
public override void Execute()
{
//initializing pp-information-holding sorted list
var displayPlays = new List<UserPlayInfo>();
//initialize the information from the top 100 plays, held in a dynamic
var ruleset = LegacyHelper.GetRulesetFromLegacyID(Ruleset ?? 0);
foreach (var play in getJsonFromApi($"get_user_best?k={Key}&u={ProfileName}&m={Ruleset}&limit=100&type=username"))
{
string beatmapID = play.beatmap_id;
string cachePath = Path.Combine("cache", $"{beatmapID}.osu");
if (!File.Exists(cachePath))
new FileWebRequest(cachePath, $"{base_url}/osu/{beatmapID}").Perform();
Mod[] mods = ruleset.ConvertLegacyMods((LegacyMods)play.enabled_mods).ToArray();
var working = new ProcessorWorkingBeatmap(cachePath, (int)play.beatmap_id) { Mods = { Value = mods } };
var score = new ProcessorScoreParser(working).Parse(new ScoreInfo
{
Ruleset = ruleset.RulesetInfo,
MaxCombo = play.maxcombo,
Mods = mods,
Statistics = new Dictionary<HitResult, int>
{
{ HitResult.Perfect, 0 },
{ HitResult.Great, (int)play.count300 },
{ HitResult.Good, (int)play.count100 },
{ HitResult.Ok, 0 },
{ HitResult.Meh, (int)play.count50 },
{ HitResult.Miss, (int)play.countmiss }
}
});
var thisPlay = new UserPlayInfo
{
Beatmap = working.BeatmapInfo,
LocalPP = ruleset.CreatePerformanceCalculator(working, score.ScoreInfo).Calculate(),
LivePP = play.pp,
Mods = mods.Length > 0 ? mods.Select(m => m.Acronym).Aggregate((c, n) => $"{c}, {n}") : "None"
};
displayPlays.Add(thisPlay);
}
dynamic userData = getJsonFromApi($"get_user?k={Key}&u={ProfileName}&m={Ruleset}&type=username")[0];
var localOrdered = displayPlays.OrderByDescending(p => p.LocalPP).ToList();
var liveOrdered = displayPlays.OrderByDescending(p => p.LivePP).ToList();
int index = 0;
double totalLocalPP = localOrdered.Sum(play => Math.Pow(0.95, index++) * play.LocalPP);
double totalLivePP = userData.pp_raw;
index = 0;
double nonBonusLivePP = liveOrdered.Sum(play => Math.Pow(0.95, index++) * play.LivePP);
//todo: implement properly. this is pretty damn wrong.
var playcountBonusPP = (totalLivePP - nonBonusLivePP);
totalLocalPP += playcountBonusPP;
OutputDocument(new Document(
new Span($"User: {userData.username}"), "\n",
new Span($"Live PP: {totalLivePP:F1} (including {playcountBonusPP:F1}pp from playcount)"), "\n",
new Span($"Local PP: {totalLocalPP:F1}"), "\n",
new Grid
{
Columns = { GridLength.Auto, GridLength.Auto, GridLength.Auto, GridLength.Auto, GridLength.Auto },
Children =
{
new Cell("beatmap"),
new Cell("live pp"),
new Cell("local pp"),
new Cell("pp change"),
new Cell("position change"),
localOrdered.Select(item => new[]
{
new Cell($"{item.Beatmap.OnlineBeatmapID} - {item.Beatmap}"),
new Cell($"{item.LivePP:F1}") { Align = Align.Right },
new Cell($"{item.LocalPP:F1}") { Align = Align.Right },
new Cell($"{item.LocalPP - item.LivePP:F1}") { Align = Align.Right },
new Cell($"{liveOrdered.IndexOf(item) - localOrdered.IndexOf(item):+0;-0;-}") { Align = Align.Center },
})
}
}
));
}
private dynamic getJsonFromApi(string request)
{
var req = new JsonWebRequest<dynamic>($"{base_url}/api/{request}");
req.Perform();
return req.ResponseObject;
}
}
}

View file

@ -1,145 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-tools/master/LICENCE
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Alba.CsConsoleFormat;
using osu.Framework.IO.Network;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace PerformanceCalculator.Profile
{
public class ProfileProcessor : IProcessor
{
private readonly ProfileCommand command;
private const string base_url = "https://osu.ppy.sh";
public ProfileProcessor(ProfileCommand command)
{
this.command = command;
}
public void Execute()
{
//initializing pp-information-holding sorted list
var displayPlays = new List<UserPlayInfo>();
//initialize the information from the top 100 plays, held in a dynamic
var ruleset = LegacyHelper.GetRulesetFromLegacyID(command.Ruleset ?? 0);
foreach (var play in getJsonFromApi($"get_user_best?k={command.Key}&u={command.ProfileName}&m={command.Ruleset}&limit=100&type=username"))
{
string beatmapID = play.beatmap_id;
string cachePath = Path.Combine("cache", $"{beatmapID}.osu");
if (!File.Exists(cachePath))
new FileWebRequest(cachePath, $"{base_url}/osu/{beatmapID}").Perform();
Mod[] mods = ruleset.ConvertLegacyMods((LegacyMods)play.enabled_mods).ToArray();
var working = new ProcessorWorkingBeatmap(cachePath, (int)play.beatmap_id) { Mods = { Value = mods } };
var score = new ProcessorScoreParser(working).Parse(new ScoreInfo
{
Ruleset = ruleset.RulesetInfo,
MaxCombo = play.maxcombo,
Mods = mods,
Statistics = new Dictionary<HitResult, int>
{
{ HitResult.Perfect, 0 },
{ HitResult.Great, (int)play.count300 },
{ HitResult.Good, (int)play.count100 },
{ HitResult.Ok, 0 },
{ HitResult.Meh, (int)play.count50 },
{ HitResult.Miss, (int)play.countmiss }
}
});
var thisPlay = new UserPlayInfo
{
Beatmap = working.BeatmapInfo,
LocalPP = ruleset.CreatePerformanceCalculator(working, score.ScoreInfo).Calculate(),
LivePP = play.pp,
Mods = mods.Length > 0 ? mods.Select(m => m.Acronym).Aggregate((c, n) => $"{c}, {n}") : "None"
};
displayPlays.Add(thisPlay);
}
dynamic userData = getJsonFromApi($"get_user?k={command.Key}&u={command.ProfileName}&m={command.Ruleset}&type=username")[0];
var localOrdered = displayPlays.OrderByDescending(p => p.LocalPP).ToList();
var liveOrdered = displayPlays.OrderByDescending(p => p.LivePP).ToList();
int index = 0;
double totalLocalPP = localOrdered.Sum(play => Math.Pow(0.95, index++) * play.LocalPP);
double totalLivePP = userData.pp_raw;
index = 0;
double nonBonusLivePP = liveOrdered.Sum(play => Math.Pow(0.95, index++) * play.LivePP);
//todo: implement properly. this is pretty damn wrong.
var playcountBonusPP = (totalLivePP - nonBonusLivePP);
totalLocalPP += playcountBonusPP;
outputDocument(new Document(
new Span($"User: {userData.username}"), "\n",
new Span($"Live PP: {totalLivePP:F1} (including {playcountBonusPP:F1}pp from playcount)"), "\n",
new Span($"Local PP: {totalLocalPP:F1}"), "\n",
new Grid
{
Columns = { GridLength.Auto, GridLength.Auto, GridLength.Auto, GridLength.Auto, GridLength.Auto },
Children =
{
new Cell("beatmap"),
new Cell("live pp"),
new Cell("local pp"),
new Cell("pp change"),
new Cell("position change"),
localOrdered.Select(item => new[]
{
new Cell($"{item.Beatmap.OnlineBeatmapID} - {item.Beatmap}"),
new Cell($"{item.LivePP:F1}") { Align = Align.Right },
new Cell($"{item.LocalPP:F1}") { Align = Align.Right },
new Cell($"{item.LocalPP - item.LivePP:F1}") { Align = Align.Right },
new Cell($"{liveOrdered.IndexOf(item) - localOrdered.IndexOf(item):+0;-0;-}") { Align = Align.Center },
})
}
}
));
}
private void outputDocument(Document document)
{
// todo: make usable by other command
using (var writer = new StringWriter())
{
ConsoleRenderer.RenderDocumentToText(document, new TextRenderTarget(writer));
var str = writer.GetStringBuilder().ToString();
var lines = str.Split('\n');
for (int i = 0; i < lines.Length; i++)
lines[i] = lines[i].TrimEnd();
str = string.Join('\n', lines);
Console.Write(str);
if (command.OutputFile != null)
File.WriteAllText(command.OutputFile, str);
}
}
private dynamic getJsonFromApi(string request)
{
var req = new JsonWebRequest<dynamic>($"{base_url}/api/{request}");
req.Perform();
return req.ResponseObject;
}
}
}

View file

@ -15,8 +15,9 @@ namespace PerformanceCalculator
[Subcommand("difficulty", typeof(DifficultyCommand))]
[Subcommand("performance", typeof(PerformanceCommand))]
[Subcommand("profile", typeof(ProfileCommand))]
[Subcommand("simulate", typeof(SimulateCommand))]
public class Program : CommandBase
[Subcommand("simulate", typeof(SimulateListing))]
[HelpOption("-?|-h|--help")]
public class Program
{
public static void Main(string[] args)
{

View file

@ -1,39 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-tools/master/LICENCE
using JetBrains.Annotations;
using osu.Game.Rulesets;
namespace PerformanceCalculator.Simulate
{
public abstract class BaseSimulateCommand : ProcessorCommand
{
public abstract string Beatmap { get; }
public abstract Ruleset Ruleset { get; }
[UsedImplicitly]
public virtual double Accuracy { get; }
[UsedImplicitly]
public virtual int? Combo { get; }
[UsedImplicitly]
public virtual double PercentCombo { get; }
[UsedImplicitly]
public virtual int Score { get; }
[UsedImplicitly]
public virtual string[] Mods { get; }
[UsedImplicitly]
public virtual int Misses { get; }
[UsedImplicitly]
public virtual int? Mehs { get; }
[UsedImplicitly]
public virtual int? Goods { get; }
}
}

View file

@ -1,97 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-tools/master/LICENCE
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using McMaster.Extensions.CommandLineUtils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace PerformanceCalculator.Simulate
{
public abstract class BaseSimulateProcessor : IProcessor
{
private readonly BaseSimulateCommand command;
protected BaseSimulateProcessor(BaseSimulateCommand command)
{
this.command = command;
}
public void Execute()
{
var ruleset = command.Ruleset;
var mods = getMods(ruleset).ToArray();
var workingBeatmap = new ProcessorWorkingBeatmap(command.Beatmap);
workingBeatmap.Mods.Value = mods;
var beatmap = workingBeatmap.GetPlayableBeatmap(ruleset.RulesetInfo);
var beatmapMaxCombo = GetMaxCombo(beatmap);
var maxCombo = command.Combo ?? (int)Math.Round(command.PercentCombo / 100 * beatmapMaxCombo);
var statistics = GenerateHitResults(command.Accuracy / 100, beatmap, command.Misses, command.Mehs, command.Goods);
var score = command.Score;
var accuracy = GetAccuracy(statistics);
var scoreInfo = new ScoreInfo
{
Accuracy = accuracy,
MaxCombo = maxCombo,
Statistics = statistics,
Mods = mods,
TotalScore = score
};
var categoryAttribs = new Dictionary<string, double>();
double pp = ruleset.CreatePerformanceCalculator(workingBeatmap, scoreInfo).Calculate(categoryAttribs);
command.Console.WriteLine(workingBeatmap.BeatmapInfo.ToString());
WritePlayInfo(scoreInfo, beatmap);
WriteAttribute("Mods", mods.Length > 0
? mods.Select(m => m.Acronym).Aggregate((c, n) => $"{c}, {n}")
: "None");
foreach (var kvp in categoryAttribs)
WriteAttribute(kvp.Key, kvp.Value.ToString(CultureInfo.InvariantCulture));
WriteAttribute("pp", pp.ToString(CultureInfo.InvariantCulture));
}
private List<Mod> getMods(Ruleset ruleset)
{
var mods = new List<Mod>();
if (command.Mods == null)
return mods;
var availableMods = ruleset.GetAllMods().ToList();
foreach (var modString in command.Mods)
{
Mod newMod = availableMods.FirstOrDefault(m => string.Equals(m.Acronym, modString, StringComparison.CurrentCultureIgnoreCase));
if (newMod == null)
throw new ArgumentException($"Invalid mod provided: {modString}");
mods.Add(newMod);
}
return mods;
}
protected abstract void WritePlayInfo(ScoreInfo scoreInfo, IBeatmap beatmap);
protected abstract int GetMaxCombo(IBeatmap beatmap);
protected abstract Dictionary<HitResult, int> GenerateHitResults(double accuracy, IBeatmap beatmap, int countMiss, int? countMeh, int? countGood);
protected virtual double GetAccuracy(Dictionary<HitResult, int> statistics) => 0;
protected void WriteAttribute(string name, string value) => command.Console.WriteLine($"{name.PadRight(15)}: {value}");
}
}

View file

@ -1,42 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-tools/master/LICENCE
using System.Collections.Generic;
using System.Globalization;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace PerformanceCalculator.Simulate.Mania
{
public class ManiaSimulateProcessor : BaseSimulateProcessor
{
protected override int GetMaxCombo(IBeatmap beatmap) => 0;
protected override Dictionary<HitResult, int> GenerateHitResults(double accuracy, IBeatmap beatmap, int countMiss, int? countMeh, int? countGood)
{
var totalHits = beatmap.HitObjects.Count;
// Only total number of hits is considered currently, so specifics don't matter
return new Dictionary<HitResult, int>
{
{ HitResult.Perfect, totalHits },
{ HitResult.Great, 0 },
{ HitResult.Ok, 0 },
{ HitResult.Good, 0 },
{ HitResult.Meh, 0 },
{ HitResult.Miss, 0 }
};
}
protected override void WritePlayInfo(ScoreInfo scoreInfo, IBeatmap beatmap)
{
WriteAttribute("Score", scoreInfo.TotalScore.ToString(CultureInfo.InvariantCulture));
}
public ManiaSimulateProcessor(BaseSimulateCommand command)
: base(command)
{
}
}
}

View file

@ -1,16 +1,21 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-tools/master/LICENCE
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using JetBrains.Annotations;
using McMaster.Extensions.CommandLineUtils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace PerformanceCalculator.Simulate.Mania
namespace PerformanceCalculator.Simulate
{
[Command(Name = "simulate mania", Description = "Computes the performance (pp) of a simulated osu!mania play.")]
public class ManiaSimulateCommand : BaseSimulateCommand
public class ManiaSimulateCommand : SimulateCommand
{
[UsedImplicitly]
[Required, FileExists]
@ -28,6 +33,27 @@ namespace PerformanceCalculator.Simulate.Mania
public override Ruleset Ruleset => new ManiaRuleset();
protected override IProcessor CreateProcessor() => new ManiaSimulateProcessor(this);
protected override int GetMaxCombo(IBeatmap beatmap) => 0;
protected override Dictionary<HitResult, int> GenerateHitResults(double accuracy, IBeatmap beatmap, int countMiss, int? countMeh, int? countGood)
{
var totalHits = beatmap.HitObjects.Count;
// Only total number of hits is considered currently, so specifics don't matter
return new Dictionary<HitResult, int>
{
{ HitResult.Perfect, totalHits },
{ HitResult.Great, 0 },
{ HitResult.Ok, 0 },
{ HitResult.Good, 0 },
{ HitResult.Meh, 0 },
{ HitResult.Miss, 0 }
};
}
protected override void WritePlayInfo(ScoreInfo scoreInfo, IBeatmap beatmap)
{
WriteAttribute("Score", scoreInfo.TotalScore.ToString(CultureInfo.InvariantCulture));
}
}
}

View file

@ -1,55 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-tools/master/LICENCE
using System.ComponentModel.DataAnnotations;
using JetBrains.Annotations;
using McMaster.Extensions.CommandLineUtils;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
namespace PerformanceCalculator.Simulate.Osu
{
[Command(Name = "simulate osu", Description = "Computes the performance (pp) of a simulated osu! play.")]
public class OsuSimulateCommand : BaseSimulateCommand
{
[UsedImplicitly]
[Required, FileExists]
[Argument(0, Name = "beatmap", Description = "Required. The beatmap file (.osu).")]
public override string Beatmap { get; }
[UsedImplicitly]
[Option(Template = "-a|--accuracy <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 <combo>", Description = "Maximum combo during play. Defaults to beatmap maximum.")]
public override int? Combo { get; }
[UsedImplicitly]
[Option(Template = "-C|--percent-combo <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 <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 <misses>", Description = "Number of misses. Defaults to 0.")]
public override int Misses { get; }
[UsedImplicitly]
[Option(Template = "-M|--mehs <mehs>", Description = "Number of mehs. Will override accuracy if used. Otherwise is automatically calculated.")]
public override int? Mehs { get; }
[UsedImplicitly]
[Option(Template = "-G|--goods <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 IProcessor CreateProcessor() => new OsuSimulateProcessor(this);
}
}

View file

@ -3,17 +3,61 @@
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.Osu
namespace PerformanceCalculator.Simulate
{
public class OsuSimulateProcessor : BaseSimulateProcessor
[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 <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 <combo>", Description = "Maximum combo during play. Defaults to beatmap maximum.")]
public override int? Combo { get; }
[UsedImplicitly]
[Option(Template = "-C|--percent-combo <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 <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 <misses>", Description = "Number of misses. Defaults to 0.")]
public override int Misses { get; }
[UsedImplicitly]
[Option(Template = "-M|--mehs <mehs>", Description = "Number of mehs. Will override accuracy if used. Otherwise is automatically calculated.")]
public override int? Mehs { get; }
[UsedImplicitly]
[Option(Template = "-G|--goods <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<Slider>().Sum(s => s.NestedHitObjects.Count - 1);
protected override Dictionary<HitResult, int> GenerateHitResults(double accuracy, IBeatmap beatmap, int countMiss, int? countMeh, int? countGood)
@ -72,10 +116,5 @@ namespace PerformanceCalculator.Simulate.Osu
WriteAttribute(Enum.GetName(typeof(HitResult), statistic.Key), statistic.Value.ToString(CultureInfo.InvariantCulture));
}
}
public OsuSimulateProcessor(BaseSimulateCommand command)
: base(command)
{
}
}
}

View file

@ -1,24 +1,119 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-tools/master/LICENCE
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using JetBrains.Annotations;
using McMaster.Extensions.CommandLineUtils;
using PerformanceCalculator.Simulate.Mania;
using PerformanceCalculator.Simulate.Osu;
using PerformanceCalculator.Simulate.Taiko;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace PerformanceCalculator.Simulate
{
[Command(Name = "performance", Description = "Computes the performance (pp) of a simulated play.")]
[Subcommand("osu", typeof(OsuSimulateCommand))]
[Subcommand("taiko", typeof(TaikoSimulateCommand))]
[Subcommand("mania", typeof(ManiaSimulateCommand))]
public class SimulateCommand : CommandBase
public abstract class SimulateCommand : ProcessorCommand
{
public int OnExecute(CommandLineApplication app, IConsole console)
public abstract string Beatmap { get; }
public abstract Ruleset Ruleset { get; }
[UsedImplicitly]
public virtual double Accuracy { get; }
[UsedImplicitly]
public virtual int? Combo { get; }
[UsedImplicitly]
public virtual double PercentCombo { get; }
[UsedImplicitly]
public virtual int Score { get; }
[UsedImplicitly]
public virtual string[] Mods { get; }
[UsedImplicitly]
public virtual int Misses { get; }
[UsedImplicitly]
public virtual int? Mehs { get; }
[UsedImplicitly]
public virtual int? Goods { get; }
public override void Execute()
{
console.WriteLine("You must specify a subcommand.");
app.ShowHelp();
return 1;
var ruleset = Ruleset;
var mods = getMods(ruleset).ToArray();
var workingBeatmap = new ProcessorWorkingBeatmap(Beatmap);
workingBeatmap.Mods.Value = mods;
var beatmap = workingBeatmap.GetPlayableBeatmap(ruleset.RulesetInfo);
var beatmapMaxCombo = GetMaxCombo(beatmap);
var maxCombo = Combo ?? (int)Math.Round(PercentCombo / 100 * beatmapMaxCombo);
var statistics = GenerateHitResults(Accuracy / 100, beatmap, Misses, Mehs, Goods);
var score = Score;
var accuracy = GetAccuracy(statistics);
var scoreInfo = new ScoreInfo
{
Accuracy = accuracy,
MaxCombo = maxCombo,
Statistics = statistics,
Mods = mods,
TotalScore = score
};
var categoryAttribs = new Dictionary<string, double>();
double pp = ruleset.CreatePerformanceCalculator(workingBeatmap, scoreInfo).Calculate(categoryAttribs);
Console.WriteLine(workingBeatmap.BeatmapInfo.ToString());
WritePlayInfo(scoreInfo, beatmap);
WriteAttribute("Mods", mods.Length > 0
? mods.Select(m => m.Acronym).Aggregate((c, n) => $"{c}, {n}")
: "None");
foreach (var kvp in categoryAttribs)
WriteAttribute(kvp.Key, kvp.Value.ToString(CultureInfo.InvariantCulture));
WriteAttribute("pp", pp.ToString(CultureInfo.InvariantCulture));
}
private List<Mod> getMods(Ruleset ruleset)
{
var mods = new List<Mod>();
if (Mods == null)
return mods;
var availableMods = ruleset.GetAllMods().ToList();
foreach (var modString in Mods)
{
Mod newMod = availableMods.FirstOrDefault(m => string.Equals(m.Acronym, modString, StringComparison.CurrentCultureIgnoreCase));
if (newMod == null)
throw new ArgumentException($"Invalid mod provided: {modString}");
mods.Add(newMod);
}
return mods;
}
protected abstract void WritePlayInfo(ScoreInfo scoreInfo, IBeatmap beatmap);
protected abstract int GetMaxCombo(IBeatmap beatmap);
protected abstract Dictionary<HitResult, int> GenerateHitResults(double accuracy, IBeatmap beatmap, int countMiss, int? countMeh, int? countGood);
protected virtual double GetAccuracy(Dictionary<HitResult, int> statistics) => 0;
protected void WriteAttribute(string name, string value) => Console.WriteLine($"{name.PadRight(15)}: {value}");
}
}

View file

@ -0,0 +1,23 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-tools/master/LICENCE
using JetBrains.Annotations;
using McMaster.Extensions.CommandLineUtils;
namespace PerformanceCalculator.Simulate
{
[Command(Name = "performance", Description = "Computes the performance (pp) of a simulated play.")]
[Subcommand("osu", typeof(OsuSimulateCommand))]
[Subcommand("taiko", typeof(TaikoSimulateCommand))]
[Subcommand("mania", typeof(ManiaSimulateCommand))]
public class SimulateListing
{
[UsedImplicitly]
public int OnExecute(CommandLineApplication app, IConsole console)
{
console.WriteLine("You must specify a subcommand.");
app.ShowHelp();
return 1;
}
}
}

View file

@ -1,51 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-tools/master/LICENCE
using System.ComponentModel.DataAnnotations;
using JetBrains.Annotations;
using McMaster.Extensions.CommandLineUtils;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Taiko;
namespace PerformanceCalculator.Simulate.Taiko
{
[Command(Name = "simulate taiko", Description = "Computes the performance (pp) of a simulated osu!taiko play.")]
public class TaikoSimulateCommand : BaseSimulateCommand
{
[UsedImplicitly]
[Required, FileExists]
[Argument(0, Name = "beatmap", Description = "Required. The beatmap file (.osu).")]
public override string Beatmap { get; }
[UsedImplicitly]
[Option(Template = "-a|--accuracy <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 <combo>", Description = "Maximum combo during play. Defaults to beatmap maximum.")]
public override int? Combo { get; }
[UsedImplicitly]
[Option(Template = "-C|--percent-combo <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 <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 <misses>", Description = "Number of misses. Defaults to 0.")]
public override int Misses { get; }
[UsedImplicitly]
[Option(Template = "-G|--goods <goods>", Description = "Number of goods. Will override accuracy if used. Otherwise is automatically calculated.")]
public override int? Goods { get; }
public override Ruleset Ruleset => new TaikoRuleset();
protected override IProcessor CreateProcessor() => new TaikoSimulateProcessor(this);
}
}

View file

@ -3,17 +3,57 @@
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.Scoring;
using osu.Game.Rulesets.Taiko;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Scoring;
namespace PerformanceCalculator.Simulate.Taiko
namespace PerformanceCalculator.Simulate
{
public class TaikoSimulateProcessor : BaseSimulateProcessor
[Command(Name = "simulate taiko", Description = "Computes the performance (pp) of a simulated osu!taiko play.")]
public class TaikoSimulateCommand : SimulateCommand
{
[UsedImplicitly]
[Required, FileExists]
[Argument(0, Name = "beatmap", Description = "Required. The beatmap file (.osu).")]
public override string Beatmap { get; }
[UsedImplicitly]
[Option(Template = "-a|--accuracy <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 <combo>", Description = "Maximum combo during play. Defaults to beatmap maximum.")]
public override int? Combo { get; }
[UsedImplicitly]
[Option(Template = "-C|--percent-combo <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 <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 <misses>", Description = "Number of misses. Defaults to 0.")]
public override int Misses { get; }
[UsedImplicitly]
[Option(Template = "-G|--goods <goods>", Description = "Number of goods. Will override accuracy if used. Otherwise is automatically calculated.")]
public override int? Goods { get; }
public override Ruleset Ruleset => new TaikoRuleset();
protected override int GetMaxCombo(IBeatmap beatmap) => beatmap.HitObjects.OfType<Hit>().Count();
protected override Dictionary<HitResult, int> GenerateHitResults(double accuracy, IBeatmap beatmap, int countMiss, int? countMeh, int? countGood)
@ -62,10 +102,5 @@ namespace PerformanceCalculator.Simulate.Taiko
WriteAttribute("Goods", scoreInfo.Statistics[HitResult.Good].ToString(CultureInfo.InvariantCulture));
WriteAttribute("Greats", scoreInfo.Statistics[HitResult.Great].ToString(CultureInfo.InvariantCulture));
}
public TaikoSimulateProcessor(BaseSimulateCommand command)
: base(command)
{
}
}
}