11using System . CommandLine ;
22using System . Text ;
33using System . Text . RegularExpressions ;
4+ using MuConvert . chu ;
45using MuConvert . mai ;
56using MuConvert . utils ;
67
@@ -40,51 +41,51 @@ private static Command BuildRootCommand()
4041 {
4142 var root = new RootCommand
4243 {
43- Description = $ "MuConvert { Utils . AppVersion } — 新一代Simai与MA2互转转谱器\n "
44+ Description = $ "MuConvert { Utils . AppVersion } — 新一代多功能音游转谱器\n " +
45+ $ "使用文档详见:https://github.com/MuNET-OSS/MuConvert/blob/master/README.md"
4446 } ;
4547
4648 var levelsOption = new Option < string ? > ( "--levels" , "-l" )
4749 {
48- Description = "仅转换指定难度(以maidata中的&inote_编号为准) ,多个难度用逗号分隔;省略则转换全部难度。" ,
50+ Description = "仅转换指定难度,多个难度用逗号分隔;省略则转换全部难度。" ,
4951 HelpName = "N[,N...]"
5052 } ;
5153
54+ var targetOption = new Option < string ? > ( "--target" , "-t" )
55+ {
56+ Description = "强制指定输出格式。目前仅有C2S->SUS必须指定本参数,其他情况省略使用默认值即可。" ,
57+ HelpName = "format"
58+ } ;
59+
5260 var outputOption = new Option < string ? > ( "--output" , "-o" )
5361 {
54- Description =
55- "输出位置:\n " +
56- "· 省略:写入输入文件同目录,文件名按默认规则(maidata.txt、lv_N.ma2 等)。\n " +
57- "· 目录:写入该目录,文件名同上按默认规则。\n " +
58- "· 文件:仅当本次转换只会生成一个输出文件时可用;扩展名须为 .txt(输出 maidata)或 .ma2(输出 MA2)。\n " +
59- "· \" -\" :仅当本次转换只会生成一个输出文件时可用;将输出内容写到stdout。" ,
62+ Description = "指定输出位置。可指定文件或目录,或\" -\" (stdout);不指定则默认为输入文件所在目录。" ,
6063 HelpName = "path"
6164 } ;
6265
6366 var strictOption = new Option < bool > ( "--strict" )
6467 {
65- Description = "Simai转MA2时, 解析使用严格模式。不可与 --lax 同时使用。 " ,
68+ Description = "解析使用严格模式(仅在Simai转MA2模式下有效) " ,
6669 Arity = ArgumentArity . ZeroOrOne ,
6770 DefaultValueFactory = _ => false
6871 } ;
6972
7073 var laxOption = new Option < bool > ( "--lax" )
7174 {
72- Description = "Simai转MA2时, 解析使用宽松模式。不可与 --strict 同时使用。 " ,
75+ Description = "解析使用宽松模式(仅在Simai转MA2模式下有效) " ,
7376 Arity = ArgumentArity . ZeroOrOne ,
7477 DefaultValueFactory = _ => false
7578 } ;
7679
7780 var inputArgument = new Argument < string > ( "path" )
7881 {
79- Description = "可以输入以下几种情况:\n " +
80- "1.单个.txt文件(标准maidata.txt,或是不含maidata的头信息、直接是Simai的Notes的文件,都可以)。会把它转为MA2。请通过-l指定要转换的谱面难度,不指定则默认转换全部难度。\n " +
81- "2.单个.ma2文件。会把它转为Simai,输出maidata.txt。如果想要转换多个难度,请传入目录,详见第4条。\n " +
82- "3.一个包含有maidata.txt的目录。行为同第一条。\n " +
83- "4.一个包含有一个或多个.ma2文件的目录。会把它们转为一个maidata.txt。请通过-l指定要转换的谱面难度,不指定则默认转换全部难度。" ,
82+ Description = "可以输入文件或目录。会自动根据输入的类型,智能执行相应的转换程序。\n " +
83+ "例如,输入一个包含多个.ma2文件的目录,则会把各个难度合并转为一个maidata.txt。" ,
8484 Arity = ArgumentArity . ExactlyOne
8585 } ;
8686
8787 root . Options . Add ( levelsOption ) ;
88+ root . Options . Add ( targetOption ) ;
8889 root . Options . Add ( outputOption ) ;
8990 root . Options . Add ( strictOption ) ;
9091 root . Options . Add ( laxOption ) ;
@@ -95,6 +96,8 @@ private static Command BuildRootCommand()
9596 var inputPath = parseResult . GetValue ( inputArgument )
9697 ?? throw new InvalidOperationException ( "缺少参数 path。" ) ;
9798 var levelsRaw = parseResult . GetValue ( levelsOption ) ;
99+ var targetRaw = parseResult . GetValue ( targetOption ) ;
100+ _cliTargetNormalized = string . IsNullOrWhiteSpace ( targetRaw ) ? null : targetRaw . Trim ( ) . ToLowerInvariant ( ) ;
98101 _outputSpec = OutputSpec . Parse ( parseResult . GetValue ( outputOption ) ) ;
99102
100103 var cliStrict = parseResult . GetValue ( strictOption ) ;
@@ -112,6 +115,9 @@ private static Command BuildRootCommand()
112115 /// <summary>由 CLI 在每次 <c>SetAction</c> 入口赋值;转换逻辑只读此字段。</summary>
113116 private static OutputSpec _outputSpec ;
114117 private static SimaiParser . StrictLevelEnum _simaiStrictLevel = SimaiParser . StrictLevelEnum . Normal ;
118+
119+ /// <summary>由 CLI 赋值;为 null 表示按输入类型使用默认输出格式,否则为小写的目标格式名(如 sus、ma2)。</summary>
120+ private static string ? _cliTargetNormalized ;
115121
116122 private enum OutputSinkKind { Default , Stdout , Directory , File }
117123
@@ -149,6 +155,8 @@ private static void RunConvert(string inputPath, string? levelsRaw)
149155 else
150156 throw new ArgumentException ( $ "找不到路径: { inputPath } ") ;
151157 }
158+
159+ private static readonly string [ ] supportedPostfixs = new [ ] { "maidata.txt" , ".ma2" , ".c2s" , ".ugc" , ".sus" } ;
152160
153161 private static void RunConvertDirectory ( string dir , string ? levelsRaw )
154162 {
@@ -158,28 +166,22 @@ private static void RunConvertDirectory(string dir, string? levelsRaw)
158166 MatchCasing = MatchCasing . CaseInsensitive ,
159167 RecurseSubdirectories = false
160168 } ;
169+ var inputPaths = Directory . EnumerateFiles ( dir , "*" , enumOpts )
170+ . Where ( file => supportedPostfixs . Any ( file . EndsWith ) ) . ToArray ( ) ;
161171
162- var maidataPaths = Directory . GetFiles ( dir , "maidata.txt" , enumOpts ) ;
163- var ma2Paths = Directory . GetFiles ( dir , "*.ma2" , enumOpts ) ;
164-
165- var hasMaidata = maidataPaths . Length > 0 ;
166- var hasMa2 = ma2Paths . Length > 0 ;
167-
168- if ( hasMaidata && hasMa2 )
169- throw new ArgumentException ( "目录中同时存在 maidata.txt 与 .ma2,请只保留其中一种输入。" ) ;
170- if ( ! hasMaidata && ! hasMa2 )
171- throw new ArgumentException ( "目录中未找到 maidata.txt 或 .ma2 文件。" ) ;
172-
173- if ( hasMaidata )
172+ if ( inputPaths . Length > 1 )
174173 {
175- if ( maidataPaths . Length > 1 )
176- throw new ArgumentException ( "目录中存在多个 maidata.txt,请只保留一个。" ) ;
177- RunConvertTxtFile ( maidataPaths [ 0 ] , levelsRaw ) ;
178- return ;
174+ if ( inputPaths . All ( file=> file . EndsWith ( ".ma2" ) ) )
175+ { // 只有多个MA2这种情况是允许的,直接调用ConvertMa2PathsToMaidata
176+ var title = new DirectoryInfo ( dir ) . Name ;
177+ ConvertMa2PathsToMaidata ( dir , title , inputPaths , levelsRaw ) ;
178+ }
179+ else
180+ {
181+ throw new ArgumentException ( $ "目录中存在多种/多个谱面文件:{ string . Join ( ", " , inputPaths ) } 。请直接指定到具体的文件路径,或者删除多余的文件。") ;
182+ }
179183 }
180-
181- var title = new DirectoryInfo ( dir ) . Name ;
182- ConvertMa2PathsToMaidata ( dir , title , ma2Paths , levelsRaw ) ;
184+ else RunConvertFile ( inputPaths [ 0 ] , levelsRaw ) ;
183185 }
184186
185187 private static void RunConvertFile ( string filePath , string ? levelsRaw )
@@ -199,7 +201,18 @@ private static void RunConvertFile(string filePath, string? levelsRaw)
199201 return ;
200202 }
201203
202- throw new ArgumentException ( $ "不支持的输入扩展名「{ ext } 」。支持 .txt、.ma2,或目录。") ;
204+ if ( string . Equals ( ext , ".c2s" , StringComparison . OrdinalIgnoreCase ) ||
205+ string . Equals ( ext , ".ugc" , StringComparison . OrdinalIgnoreCase ) ||
206+ string . Equals ( ext , ".sus" , StringComparison . OrdinalIgnoreCase ) )
207+ {
208+ if ( levelsRaw != null ) throw new ArgumentException ( "-l / --levels 仅适用于 maimai 的 maidata 或目录中的 .ma2,不适用于中二谱(.c2s / .ugc / .sus)。" ) ;
209+ AssertStrictLaxOnlyForSimaiToMa2 ( " 中二谱(.c2s / .ugc / .sus)" ) ;
210+ var kind = ext . TrimStart ( '.' ) . ToLowerInvariant ( ) ;
211+ RunConvertChuSingleFile ( filePath , kind ) ;
212+ return ;
213+ }
214+
215+ throw new ArgumentException ( $ "不支持的输入扩展名「{ ext } 」。支持 .txt、.ma2、.c2s、.ugc、.sus,或目录。") ;
203216 }
204217
205218 private static void RunConvertTxtFile ( string inputPath , string ? levelsRaw )
@@ -209,6 +222,9 @@ private static void RunConvertTxtFile(string inputPath, string? levelsRaw)
209222 var inputDir = Path . GetDirectoryName ( Path . GetFullPath ( inputPath ) ) ! ;
210223 var text = File . ReadAllText ( inputPath , Encoding . UTF8 ) ;
211224
225+ var targetFormat = _cliTargetNormalized ?? "ma2" ;
226+ if ( targetFormat != "ma2" ) throw new ArgumentException ( $ "不支持的输出类型「{ targetFormat } 」。输入文件为simai时,输出格式仅支持ma2。") ;
227+
212228 if ( LooksLikeMaidata ( text ) )
213229 {
214230 var maidata = new Maidata ( text ) ;
@@ -278,8 +294,10 @@ private static void ConvertMa2PathsToMaidata(string outputDir, string title, IRe
278294 {
279295 if ( ma2FullPaths . Count == 0 )
280296 throw new ArgumentException ( "未提供任何 .ma2 文件。" ) ;
281- if ( _simaiStrictLevel != SimaiParser . StrictLevelEnum . Normal )
282- throw new ArgumentException ( "--strict / --lax 仅适用于 Simai(.txt / maidata)转 MA2,不能用于 MA2 转 Simai。" ) ;
297+ AssertStrictLaxOnlyForSimaiToMa2 ( " MA2 转 Simai" ) ;
298+
299+ var targetFormat = _cliTargetNormalized ?? "simai" ;
300+ if ( targetFormat != "simai" ) throw new ArgumentException ( $ "不支持的输出类型「{ targetFormat } 」。输入文件为ma2时,输出格式仅支持simai。") ;
283301
284302 var paths = ma2FullPaths . Select ( Path . GetFullPath ) . Distinct ( StringComparer . OrdinalIgnoreCase ) . ToArray ( ) ;
285303 var levelFilter = string . IsNullOrWhiteSpace ( levelsRaw ) ? null : ParseLevelList ( levelsRaw ) ;
@@ -300,7 +318,7 @@ private static void ConvertMa2PathsToMaidata(string outputDir, string title, IRe
300318
301319 foreach ( var ( fullPath , levelId ) in assignments )
302320 {
303- Console . Error . WriteLine ( $ "Simai → MA2 : { fullPath } (lv{ levelId } ) → { destNote } ") ;
321+ Console . Error . WriteLine ( $ "MA2 → Simai : { fullPath } (lv{ levelId } ) → { destNote } ") ;
304322 var ma2Text = File . ReadAllText ( fullPath , Encoding . UTF8 ) ;
305323 var ( chart , parseAlerts ) = new MA2Parser ( ) . Parse ( ma2Text ) ;
306324 PrintAlerts ( parseAlerts ) ;
@@ -419,6 +437,83 @@ private static void ValidateOutputFileExtension(string filePath, string required
419437 throw new ArgumentException ( $ "输出文件扩展名须为「{ requiredExt } 」,当前为「{ ( string . IsNullOrEmpty ( ext ) ? "(无)" : ext ) } 」。") ;
420438 }
421439
440+ private static void AssertStrictLaxOnlyForSimaiToMa2 ( string contextSuffix )
441+ {
442+ if ( _simaiStrictLevel != SimaiParser . StrictLevelEnum . Normal )
443+ throw new ArgumentException ( $ "--strict / --lax 仅适用于 Simai(.txt / maidata 或纯 inote)转 MA2,不能用于{ contextSuffix } 。") ;
444+ }
445+
446+ private static readonly Dictionary < string , string [ ] > chuTargetsDict = new ( )
447+ {
448+ [ "c2s" ] = [ "ugc" , "sus" ] ,
449+ [ "ugc" ] = [ "c2s" , "sus" ] ,
450+ [ "sus" ] = [ "c2s" ] ,
451+ } ;
452+
453+ private static void ValidateOutputForSingleChuText ( string inputFormat , string targetFormat )
454+ {
455+ var validTargets = chuTargetsDict . GetValueOrDefault ( inputFormat ) ?? [ ] ;
456+ if ( ! validTargets . Contains ( targetFormat ) ) throw new ArgumentException ( $ "不支持的输出类型「{ targetFormat } 」。输入文件为{ inputFormat } 时,输出格式仅支持{ validTargets } 。") ;
457+
458+ if ( _outputSpec . Kind == OutputSinkKind . Stdout ) return ;
459+ if ( _outputSpec . Kind == OutputSinkKind . File )
460+ ValidateOutputFileExtension ( _outputSpec . FsPath ! , "." + targetFormat ) ;
461+ }
462+
463+ private static void RunConvertChuSingleFile ( string filePath , string inputKind )
464+ {
465+ var targetFormat = _cliTargetNormalized ?? chuTargetsDict [ inputKind ] [ 0 ] ;
466+ ValidateOutputForSingleChuText ( inputKind , targetFormat ) ;
467+
468+ var full = Path . GetFullPath ( filePath ) ;
469+ var inputDir = Path . GetDirectoryName ( full ) ! ;
470+ var text = File . ReadAllText ( full , Encoding . UTF8 ) ;
471+
472+ var baseDir = _outputSpec . ResolveOutputDir ( inputDir ) ;
473+ var outPath = _outputSpec . Kind == OutputSinkKind . File ? _outputSpec . FsPath ! : Path . Combine ( baseDir , Path . GetFileNameWithoutExtension ( full ) + "." + targetFormat ) ;
474+ var destNote = _outputSpec . Kind == OutputSinkKind . Stdout ? "(标准输出)" : outPath ;
475+ Console . Error . WriteLine ( $ "{ inputKind . ToUpperInvariant ( ) } → { targetFormat . ToUpperInvariant ( ) } : { full } → { destNote } ") ;
476+
477+ ChuChart chart ;
478+ List < Alert > parseAlerts ;
479+ switch ( inputKind )
480+ {
481+ case "c2s" :
482+ ( chart , parseAlerts ) = new C2sParser ( ) . Parse ( text ) ;
483+ break ;
484+ case "ugc" :
485+ ( chart , parseAlerts ) = new UgcParser ( ) . Parse ( text ) ;
486+ break ;
487+ case "sus" :
488+ ( chart , parseAlerts ) = new SusParser ( ) . Parse ( text ) ;
489+ break ;
490+ default :
491+ throw new ArgumentException ( $ "内部错误:未知中二输入种类「{ inputKind } 」。") ;
492+ }
493+ PrintAlerts ( parseAlerts ) ;
494+
495+ string outText ;
496+ List < Alert > genAlerts ;
497+ switch ( targetFormat )
498+ {
499+ case "ugc" :
500+ ( outText , genAlerts ) = new UgcGenerator ( ) . Generate ( chart ) ;
501+ break ;
502+ case "sus" :
503+ ( outText , genAlerts ) = new SusGenerator ( ) . Generate ( chart ) ;
504+ break ;
505+ case "c2s" :
506+ ( outText , genAlerts ) = new C2sGenerator ( ) . Generate ( chart ) ;
507+ break ;
508+ default :
509+ throw new ArgumentException ( $ "内部错误:未实现的中二输出类型「{ targetFormat } 」。") ;
510+ }
511+ PrintAlerts ( genAlerts ) ;
512+
513+ if ( _outputSpec . Kind == OutputSinkKind . Stdout ) Console . Out . Write ( outText ) ;
514+ else File . WriteAllText ( outPath , outText , new UTF8Encoding ( encoderShouldEmitUTF8Identifier : false ) ) ;
515+ }
516+
422517 private static string SimaiToMa2 ( string inote , int clockCount = 4 , bool bigTouch = false , bool isUtage = false ,
423518 SimaiParser . StrictLevelEnum strictLevel = SimaiParser . StrictLevelEnum . Normal )
424519 {
0 commit comments