11using System . CommandLine ;
22using System . Text ;
33using System . Text . RegularExpressions ;
4+ using MuConvert . chu ;
45using MuConvert . mai ;
56using 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 ( $ "Simai → MA2 : { fullPath } (lv{ levelId } ) → { destNote } ") ;
341+ Console . Error . WriteLine ( $ "MA2 → Simai : { 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