Skip to content

Commit 4041dc3

Browse files
committed
[+] CLI for 中二
1 parent 8265abc commit 4041dc3

1 file changed

Lines changed: 135 additions & 13 deletions

File tree

Program.cs

Lines changed: 135 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.CommandLine;
22
using System.Text;
33
using System.Text.RegularExpressions;
4+
using MuConvert.chu;
45
using MuConvert.mai;
56
using MuConvert.utils;
67

@@ -50,6 +51,12 @@ private static Command BuildRootCommand()
5051
HelpName = "N[,N...]"
5152
};
5253

54+
var targetOption = new Option<string?>("--target", "-t")
55+
{
56+
Description = "强制指定输出格式。目前仅有C2S->SUS必须指定本参数,其他情况省略使用默认值即可。",
57+
HelpName = "format"
58+
};
59+
5360
var outputOption = new Option<string?>("--output", "-o")
5461
{
5562
Description = "指定输出位置。可指定文件或目录,或\"-\"(stdout);不指定则默认为输入文件所在目录。",
@@ -78,6 +85,7 @@ private static Command BuildRootCommand()
7885
};
7986

8087
root.Options.Add(levelsOption);
88+
root.Options.Add(targetOption);
8189
root.Options.Add(outputOption);
8290
root.Options.Add(strictOption);
8391
root.Options.Add(laxOption);
@@ -88,6 +96,8 @@ private static Command BuildRootCommand()
8896
var inputPath = parseResult.GetValue(inputArgument)
8997
?? throw new InvalidOperationException("缺少参数 path。");
9098
var levelsRaw = parseResult.GetValue(levelsOption);
99+
var targetRaw = parseResult.GetValue(targetOption);
100+
_cliTargetNormalized = string.IsNullOrWhiteSpace(targetRaw) ? null : targetRaw.Trim().ToLowerInvariant();
91101
_outputSpec = OutputSpec.Parse(parseResult.GetValue(outputOption));
92102

93103
var cliStrict = parseResult.GetValue(strictOption);
@@ -105,6 +115,9 @@ private static Command BuildRootCommand()
105115
/// <summary>由 CLI 在每次 <c>SetAction</c> 入口赋值;转换逻辑只读此字段。</summary>
106116
private static OutputSpec _outputSpec;
107117
private static SimaiParser.StrictLevelEnum _simaiStrictLevel = SimaiParser.StrictLevelEnum.Normal;
118+
119+
/// <summary>由 CLI 赋值;为 null 表示按输入类型使用默认输出格式,否则为小写的目标格式名(如 sus、ma2)。</summary>
120+
private static string? _cliTargetNormalized;
108121

109122
private enum OutputSinkKind { Default, Stdout, Directory, File }
110123

@@ -142,6 +155,8 @@ private static void RunConvert(string inputPath, string? levelsRaw)
142155
else
143156
throw new ArgumentException($"找不到路径: {inputPath}");
144157
}
158+
159+
private static readonly string[] chuExtentions = new[] { ".c2s", ".ugc", ".sus" };
145160

146161
private static void RunConvertDirectory(string dir, string? levelsRaw)
147162
{
@@ -154,25 +169,39 @@ private static void RunConvertDirectory(string dir, string? levelsRaw)
154169

155170
var maidataPaths = Directory.GetFiles(dir, "maidata.txt", enumOpts);
156171
var ma2Paths = Directory.GetFiles(dir, "*.ma2", enumOpts);
172+
var chuPaths = Directory.EnumerateFiles(dir, "*", enumOpts)
173+
.Where(file => chuExtentions.Contains(Path.GetExtension(file).ToLower())).ToArray();
157174

158175
var hasMaidata = maidataPaths.Length > 0;
159176
var hasMa2 = ma2Paths.Length > 0;
177+
var hasChu = chuPaths.Length > 0;
160178

161179
if (hasMaidata && hasMa2)
162180
throw new ArgumentException("目录中同时存在 maidata.txt 与 .ma2,请只保留其中一种输入。");
163-
if (!hasMaidata && !hasMa2)
164-
throw new ArgumentException("目录中未找到 maidata.txt 或 .ma2 文件。");
181+
if ((hasMaidata || hasMa2) && hasChu)
182+
throw new ArgumentException("目录中不能同时存在 maimai 谱(maidata.txt / .ma2)与中二谱(.c2s / .ugc / .sus),请分开转换。");
183+
if (!hasMaidata && !hasMa2 && !hasChu)
184+
throw new ArgumentException("目录中未找到任何支持的谱面文件");
165185

186+
string filename = "";
166187
if (hasMaidata)
167188
{
168-
if (maidataPaths.Length > 1)
169-
throw new ArgumentException("目录中存在多个 maidata.txt,请只保留一个。");
170-
RunConvertTxtFile(maidataPaths[0], levelsRaw);
171-
return;
189+
if (maidataPaths.Length > 1) throw new ArgumentException("目录中存在多个 maidata.txt,请只保留一个。");
190+
filename = maidataPaths[0];
172191
}
173-
174-
var title = new DirectoryInfo(dir).Name;
175-
ConvertMa2PathsToMaidata(dir, title, ma2Paths, levelsRaw);
192+
else if (hasMa2)
193+
{
194+
if (ma2Paths.Length > 1)
195+
{ // 多个文件,无法直接转发给RunConvertFile,故自行调用ConvertMa2PathsToMaidata
196+
var title = new DirectoryInfo(dir).Name;
197+
ConvertMa2PathsToMaidata(dir, title, ma2Paths, levelsRaw);
198+
return;
199+
}
200+
else filename = ma2Paths[0];
201+
}
202+
else filename = chuPaths[0];
203+
204+
RunConvertFile(filename, levelsRaw);
176205
}
177206

178207
private static void RunConvertFile(string filePath, string? levelsRaw)
@@ -192,7 +221,18 @@ private static void RunConvertFile(string filePath, string? levelsRaw)
192221
return;
193222
}
194223

195-
throw new ArgumentException($"不支持的输入扩展名「{ext}」。支持 .txt、.ma2,或目录。");
224+
if (string.Equals(ext, ".c2s", StringComparison.OrdinalIgnoreCase) ||
225+
string.Equals(ext, ".ugc", StringComparison.OrdinalIgnoreCase) ||
226+
string.Equals(ext, ".sus", StringComparison.OrdinalIgnoreCase))
227+
{
228+
if (levelsRaw != null) throw new ArgumentException("-l / --levels 仅适用于 maimai 的 maidata 或目录中的 .ma2,不适用于中二谱(.c2s / .ugc / .sus)。");
229+
AssertStrictLaxOnlyForSimaiToMa2(" 中二谱(.c2s / .ugc / .sus)");
230+
var kind = ext.TrimStart('.').ToLowerInvariant();
231+
RunConvertChuSingleFile(filePath, kind);
232+
return;
233+
}
234+
235+
throw new ArgumentException($"不支持的输入扩展名「{ext}」。支持 .txt、.ma2、.c2s、.ugc、.sus,或目录。");
196236
}
197237

198238
private static void RunConvertTxtFile(string inputPath, string? levelsRaw)
@@ -202,6 +242,9 @@ private static void RunConvertTxtFile(string inputPath, string? levelsRaw)
202242
var inputDir = Path.GetDirectoryName(Path.GetFullPath(inputPath))!;
203243
var text = File.ReadAllText(inputPath, Encoding.UTF8);
204244

245+
var targetFormat = _cliTargetNormalized ?? "ma2";
246+
if (targetFormat != "ma2") throw new ArgumentException($"不支持的输出类型「{targetFormat}」。输入文件为simai时,输出格式仅支持ma2。");
247+
205248
if (LooksLikeMaidata(text))
206249
{
207250
var maidata = new Maidata(text);
@@ -271,8 +314,10 @@ private static void ConvertMa2PathsToMaidata(string outputDir, string title, IRe
271314
{
272315
if (ma2FullPaths.Count == 0)
273316
throw new ArgumentException("未提供任何 .ma2 文件。");
274-
if (_simaiStrictLevel != SimaiParser.StrictLevelEnum.Normal)
275-
throw new ArgumentException("--strict / --lax 仅适用于 Simai(.txt / maidata)转 MA2,不能用于 MA2 转 Simai。");
317+
AssertStrictLaxOnlyForSimaiToMa2(" MA2 转 Simai");
318+
319+
var targetFormat = _cliTargetNormalized ?? "simai";
320+
if (targetFormat != "simai") throw new ArgumentException($"不支持的输出类型「{targetFormat}」。输入文件为ma2时,输出格式仅支持simai。");
276321

277322
var paths = ma2FullPaths.Select(Path.GetFullPath).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
278323
var levelFilter = string.IsNullOrWhiteSpace(levelsRaw) ? null : ParseLevelList(levelsRaw);
@@ -293,7 +338,7 @@ private static void ConvertMa2PathsToMaidata(string outputDir, string title, IRe
293338

294339
foreach (var (fullPath, levelId) in assignments)
295340
{
296-
Console.Error.WriteLine($"SimaiMA2: {fullPath}(lv{levelId}) → {destNote}");
341+
Console.Error.WriteLine($"MA2Simai: {fullPath}(lv{levelId}) → {destNote}");
297342
var ma2Text = File.ReadAllText(fullPath, Encoding.UTF8);
298343
var (chart, parseAlerts) = new MA2Parser().Parse(ma2Text);
299344
PrintAlerts(parseAlerts);
@@ -412,6 +457,83 @@ private static void ValidateOutputFileExtension(string filePath, string required
412457
throw new ArgumentException($"输出文件扩展名须为「{requiredExt}」,当前为「{(string.IsNullOrEmpty(ext) ? "(无)" : ext)}」。");
413458
}
414459

460+
private static void AssertStrictLaxOnlyForSimaiToMa2(string contextSuffix)
461+
{
462+
if (_simaiStrictLevel != SimaiParser.StrictLevelEnum.Normal)
463+
throw new ArgumentException($"--strict / --lax 仅适用于 Simai(.txt / maidata 或纯 inote)转 MA2,不能用于{contextSuffix}。");
464+
}
465+
466+
private static readonly Dictionary<string, string[]> chuTargetsDict = new()
467+
{
468+
["c2s"] = ["ugc", "sus"],
469+
["ugc"] = ["c2s", "sus"],
470+
["sus"] = ["c2s"],
471+
};
472+
473+
private static void ValidateOutputForSingleChuText(string inputFormat, string targetFormat)
474+
{
475+
var validTargets = chuTargetsDict.GetValueOrDefault(inputFormat) ?? [];
476+
if (!validTargets.Contains(targetFormat)) throw new ArgumentException($"不支持的输出类型「{targetFormat}」。输入文件为{inputFormat}时,输出格式仅支持{validTargets}。");
477+
478+
if (_outputSpec.Kind == OutputSinkKind.Stdout) return;
479+
if (_outputSpec.Kind == OutputSinkKind.File)
480+
ValidateOutputFileExtension(_outputSpec.FsPath!, "." + targetFormat);
481+
}
482+
483+
private static void RunConvertChuSingleFile(string filePath, string inputKind)
484+
{
485+
var targetFormat = _cliTargetNormalized ?? chuTargetsDict[inputKind][0];
486+
ValidateOutputForSingleChuText(inputKind, targetFormat);
487+
488+
var full = Path.GetFullPath(filePath);
489+
var inputDir = Path.GetDirectoryName(full)!;
490+
var text = File.ReadAllText(full, Encoding.UTF8);
491+
492+
var baseDir = _outputSpec.ResolveOutputDir(inputDir);
493+
var outPath = _outputSpec.Kind == OutputSinkKind.File ? _outputSpec.FsPath! : Path.Combine(baseDir, Path.GetFileNameWithoutExtension(full) + "." + targetFormat);
494+
var destNote = _outputSpec.Kind == OutputSinkKind.Stdout ? "(标准输出)" : outPath;
495+
Console.Error.WriteLine($"{inputKind.ToUpperInvariant()}{targetFormat.ToUpperInvariant()}: {full}{destNote}");
496+
497+
IChuChart chart;
498+
List<Alert> parseAlerts;
499+
switch (inputKind)
500+
{
501+
case "c2s":
502+
(chart, parseAlerts) = new C2sParser().Parse(text);
503+
break;
504+
case "ugc":
505+
(chart, parseAlerts) = new UgcParser().Parse(text);
506+
break;
507+
case "sus":
508+
(chart, parseAlerts) = new SusParser().Parse(text);
509+
break;
510+
default:
511+
throw new ArgumentException($"内部错误:未知中二输入种类「{inputKind}」。");
512+
}
513+
PrintAlerts(parseAlerts);
514+
515+
string outText;
516+
List<Alert> genAlerts;
517+
switch (targetFormat)
518+
{
519+
case "ugc":
520+
(outText, genAlerts) = new UgcGenerator().Generate(chart);
521+
break;
522+
case "sus":
523+
(outText, genAlerts) = new SusGenerator().Generate(chart);
524+
break;
525+
case "c2s":
526+
(outText, genAlerts) = new C2sGenerator().Generate(chart);
527+
break;
528+
default:
529+
throw new ArgumentException($"内部错误:未实现的中二输出类型「{targetFormat}」。");
530+
}
531+
PrintAlerts(genAlerts);
532+
533+
if (_outputSpec.Kind == OutputSinkKind.Stdout) Console.Out.Write(outText);
534+
else File.WriteAllText(outPath, outText, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
535+
}
536+
415537
private static string SimaiToMa2(string inote, int clockCount = 4, bool bigTouch = false, bool isUtage = false,
416538
SimaiParser.StrictLevelEnum strictLevel = SimaiParser.StrictLevelEnum.Normal)
417539
{

0 commit comments

Comments
 (0)