11using System . CommandLine ;
22using System . Text ;
3+ using System . Text . RegularExpressions ;
34using MuConvert . generator ;
45using MuConvert . maidata ;
5- using MuConvert . parser . simai ;
6+ using MuConvert . parser ;
67using MuConvert . utils ;
78
89namespace MuConvert ;
@@ -33,19 +34,22 @@ private static Command BuildRootCommand()
3334 {
3435 var root = new RootCommand
3536 {
36- Description = $ "MuConvert { Utils . AppVersion } — simai / maidata → MA2\n " +
37- "将 .txt 格式的 simai 单谱或 maidata 转为 MA2,输出与输入同目录的 lv_N.ma2。"
37+ Description = $ "MuConvert { Utils . AppVersion } — 新一代Simai ↔ MA2互转转谱器\n "
3838 } ;
3939
4040 var levelsOption = new Option < string ? > ( "--levels" , "-l" )
4141 {
42- Description = "仅转换指定难度(maidata 的 inote 编号),逗号分隔;省略则全部。纯 simai 单谱不可使用本选项 。" ,
42+ Description = "仅转换指定难度(以maidata中的&inote_编号为准),多个难度用逗号分隔;省略则转换全部难度 。" ,
4343 HelpName = "N[,N...]"
4444 } ;
4545
46- var inputArgument = new Argument < string > ( "inputfile " )
46+ var inputArgument = new Argument < string > ( "path " )
4747 {
48- Description = "输入 .txt(单谱 simai 或 maidata)" ,
48+ Description = "可以输入以下几种情况:\n " +
49+ "1.单个.txt文件(标准maidata.txt,或是不含maidata的头信息、直接是Simai的Notes的文件,都可以)。请通过-l指定要转换的谱面难度,不指定则默认转换全部难度。\n " +
50+ "2.单个.ma2文件。会把它转为Simai,输出maidata.txt。如果想要转换多个难度,请传入目录,详见第4条。\n " +
51+ "3.一个包含有maidata.txt的目录。行为同第一条。\n " +
52+ "4.一个包含有一个或多个.ma2文件的目录。会把它们转为一个maidata.txt。请通过-l指定要转换的谱面难度,不指定则默认转换全部难度。" ,
4953 Arity = ArgumentArity . ExactlyOne
5054 } ;
5155
@@ -55,7 +59,7 @@ private static Command BuildRootCommand()
5559 root . SetAction ( parseResult =>
5660 {
5761 var inputPath = parseResult . GetValue ( inputArgument )
58- ?? throw new InvalidOperationException ( "缺少参数 inputfile 。" ) ;
62+ ?? throw new InvalidOperationException ( "缺少参数 path 。" ) ;
5963 var levelsRaw = parseResult . GetValue ( levelsOption ) ;
6064 RunConvert ( inputPath , levelsRaw ) ;
6165 } ) ;
@@ -65,27 +69,162 @@ private static Command BuildRootCommand()
6569
6670 private static void RunConvert ( string inputPath , string ? levelsRaw )
6771 {
68- var levelFilter = string . IsNullOrWhiteSpace ( levelsRaw )
69- ? null
70- : ParseLevelList ( levelsRaw ) ;
72+ var fullPath = Path . GetFullPath ( inputPath . Trim ( ) ) ;
7173
72- var ext = Path . GetExtension ( inputPath ) ;
74+ if ( Directory . Exists ( fullPath ) )
75+ RunConvertDirectory ( fullPath , levelsRaw ) ;
76+ else if ( File . Exists ( fullPath ) )
77+ RunConvertFile ( fullPath , levelsRaw ) ;
78+ else
79+ throw new ArgumentException ( $ "找不到路径: { inputPath } ") ;
80+ }
81+
82+ private static void RunConvertDirectory ( string dir , string ? levelsRaw )
83+ {
84+ var enumOpts = new EnumerationOptions
85+ {
86+ IgnoreInaccessible = true ,
87+ MatchCasing = MatchCasing . CaseInsensitive ,
88+ RecurseSubdirectories = false
89+ } ;
90+
91+ var maidataPaths = Directory . GetFiles ( dir , "maidata.txt" , enumOpts ) ;
92+ var ma2Paths = Directory . GetFiles ( dir , "*.ma2" , enumOpts ) ;
93+
94+ var hasMaidata = maidataPaths . Length > 0 ;
95+ var hasMa2 = ma2Paths . Length > 0 ;
96+
97+ if ( hasMaidata && hasMa2 )
98+ throw new ArgumentException ( "目录中同时存在 maidata.txt 与 .ma2,请只保留其中一种输入。" ) ;
99+ if ( ! hasMaidata && ! hasMa2 )
100+ throw new ArgumentException ( "目录中未找到 maidata.txt 或 .ma2 文件。" ) ;
101+
102+ if ( hasMaidata )
103+ {
104+ if ( maidataPaths . Length > 1 )
105+ throw new ArgumentException ( "目录中存在多个 maidata.txt,请只保留一个。" ) ;
106+ RunConvertTxtFile ( maidataPaths [ 0 ] , levelsRaw ) ;
107+ return ;
108+ }
109+
110+ var title = new DirectoryInfo ( dir ) . Name ;
111+ ConvertMa2PathsToMaidata ( dir , title , ma2Paths , levelsRaw ) ;
112+ }
113+
114+ private static void RunConvertFile ( string filePath , string ? levelsRaw )
115+ {
116+ var ext = Path . GetExtension ( filePath ) ;
73117 if ( string . Equals ( ext , ".ma2" , StringComparison . OrdinalIgnoreCase ) )
74- throw new NotImplementedException ( "从 .ma2 输入的转换尚未实现。" ) ;
118+ {
119+ var parent = Path . GetDirectoryName ( Path . GetFullPath ( filePath ) ) ! ;
120+ var title = new DirectoryInfo ( parent ) . Name ;
121+ ConvertMa2PathsToMaidata ( parent , title , [ filePath ] , levelsRaw ) ;
122+ return ;
123+ }
124+
125+ if ( string . Equals ( ext , ".txt" , StringComparison . OrdinalIgnoreCase ) )
126+ {
127+ RunConvertTxtFile ( filePath , levelsRaw ) ;
128+ return ;
129+ }
75130
76- if ( ! string . Equals ( ext , " .txt" , StringComparison . OrdinalIgnoreCase ) )
77- throw new ArgumentException ( $ "不支持的输入扩展名「 { ext } 」。目前仅支持 .txt(simai / maidata)。" ) ;
131+ throw new ArgumentException ( $ "不支持的输入扩展名「 { ext } 」。支持 .txt、.ma2,或目录。" ) ;
132+ }
78133
79- if ( ! File . Exists ( inputPath ) )
80- throw new ArgumentException ( $ "找不到文件: { inputPath } ") ;
134+ private static void RunConvertTxtFile ( string inputPath , string ? levelsRaw )
135+ {
136+ var levelFilter = string . IsNullOrWhiteSpace ( levelsRaw ) ? null : ParseLevelList ( levelsRaw ) ;
81137
82138 var inputDir = Path . GetDirectoryName ( Path . GetFullPath ( inputPath ) ) ! ;
83139 var text = File . ReadAllText ( inputPath , Encoding . UTF8 ) ;
84140
85141 if ( LooksLikeMaidata ( text ) )
86- ConvertMaidata ( text , inputDir , levelFilter ) ;
142+ ConvertMaidata ( text , inputDir , levelFilter , inputPath ) ;
87143 else
88- ConvertPlainSimai ( text , inputDir , levelFilter ) ;
144+ ConvertPlainSimai ( text , inputDir , levelFilter , inputPath ) ;
145+ }
146+
147+ /// <summary>
148+ /// 与测试集约定一致:<c>*XX.ma2</c> 中 XX 为游戏难度后缀时,maidata inote = XX + 2;<c>lv_N.ma2</c> 为本工具导出,inote = N。
149+ /// </summary>
150+ private static bool TryParseMaidataLevelFromMa2FileName ( string filePath , out int levelId )
151+ {
152+ var stem = Path . GetFileNameWithoutExtension ( filePath ) ;
153+
154+ if ( stem . StartsWith ( "lv_" , StringComparison . OrdinalIgnoreCase ) && stem . Length > 3 &&
155+ int . TryParse ( stem . AsSpan ( 3 ) , out var lv ) && lv > 0 )
156+ {
157+ levelId = lv ;
158+ return true ;
159+ }
160+
161+ var m = Regex . Match ( stem , @"(\d{2})$" ) ;
162+ if ( m . Success && int . TryParse ( m . Groups [ 1 ] . Value , out var suffix ) )
163+ {
164+ levelId = suffix + 2 ;
165+ return true ;
166+ }
167+
168+ levelId = 5 ;
169+ return false ;
170+ }
171+
172+ private static List < ( string FullPath , int LevelId ) > AssignMaidataLevelsForMa2Files ( string [ ] ma2Paths )
173+ {
174+ Array . Sort ( ma2Paths , StringComparer . OrdinalIgnoreCase ) ;
175+ var used = new HashSet < int > ( ) ;
176+ var list = new List < ( string , int ) > ( ma2Paths . Length ) ;
177+
178+ foreach ( var path in ma2Paths )
179+ {
180+ var suggested = TryParseMaidataLevelFromMa2FileName ( path , out var parsed ) ? parsed : 5 ;
181+
182+ var id = suggested ;
183+ while ( used . Contains ( id ) )
184+ id ++ ;
185+
186+ used . Add ( id ) ;
187+ list . Add ( ( path , id ) ) ;
188+ }
189+
190+ return list ;
191+ }
192+
193+ private static void ConvertMa2PathsToMaidata ( string outputDir , string title , IReadOnlyList < string > ma2FullPaths , string ? levelsRaw )
194+ {
195+ if ( ma2FullPaths . Count == 0 )
196+ throw new ArgumentException ( "未提供任何 .ma2 文件。" ) ;
197+
198+ var paths = ma2FullPaths . Select ( Path . GetFullPath ) . Distinct ( StringComparer . OrdinalIgnoreCase ) . ToArray ( ) ;
199+ var levelFilter = string . IsNullOrWhiteSpace ( levelsRaw ) ? null : ParseLevelList ( levelsRaw ) ;
200+ var assignments = AssignMaidataLevelsForMa2Files ( paths )
201+ . OrderBy ( t => t . LevelId )
202+ . Where ( ( _ , lv ) => levelFilter == null || levelFilter . Contains ( lv ) )
203+ . ToList ( ) ;
204+ var outPath = Path . Combine ( outputDir , "maidata.txt" ) ;
205+
206+ int clockCount = 4 ;
207+ var inoteBlocks = new List < ( int LevelId , string Inote ) > ( ) ;
208+
209+ foreach ( var ( fullPath , levelId ) in assignments )
210+ {
211+ Console . WriteLine ( $ "Simai → MA2: { fullPath } (lv{ levelId } ) → { outPath } ") ;
212+ var ma2Text = File . ReadAllText ( fullPath , Encoding . UTF8 ) ;
213+ var ( chart , parseAlerts ) = new MA2Parser ( ) . Parse ( ma2Text ) ;
214+ PrintAlerts ( parseAlerts ) ;
215+ var ( simai , genAlerts ) = new SimaiGenerator ( ) . Generate ( chart ) ;
216+ PrintAlerts ( genAlerts ) ;
217+ inoteBlocks . Add ( ( levelId , simai ) ) ;
218+ clockCount = chart . ClockCount ;
219+ }
220+
221+ var maidata = new Maidata ( ) ;
222+ maidata [ "title" ] = title ;
223+ maidata [ "first" ] = "0" ;
224+ maidata [ "clock_count" ] = clockCount . ToString ( ) ;
225+ foreach ( var ( levelId , inote ) in inoteBlocks )
226+ maidata . AddLevel ( levelId , new MaidataChart ( inote ) ) ;
227+ File . WriteAllText ( outPath , maidata . ToString ( ) , new UTF8Encoding ( encoderShouldEmitUTF8Identifier : false ) ) ;
89228 }
90229
91230 private static HashSet < int > ParseLevelList ( string s )
@@ -133,7 +272,7 @@ private static void PrintAlerts(IReadOnlyList<Alert> alerts, string? header = nu
133272 Console . Error . WriteLine ( a . ToString ( ) ) ;
134273 }
135274
136- private static void ConvertMaidata ( string text , string outputDir , HashSet < int > ? levelFilter )
275+ private static void ConvertMaidata ( string text , string outputDir , HashSet < int > ? levelFilter , string inputPath )
137276 {
138277 var maidata = new Maidata ( text ) ;
139278 var ids = maidata . Levels . Keys . OrderBy ( k => k ) . ToList ( ) ;
@@ -149,34 +288,34 @@ private static void ConvertMaidata(string text, string outputDir, HashSet<int>?
149288
150289 foreach ( var id in selected )
151290 {
291+ var outPath = Path . Combine ( outputDir , $ "lv_{ id } .ma2") ;
292+ Console . WriteLine ( $ "Simai → MA2: { inputPath } (lv${ id } ) → { outPath } ") ;
152293 var chartInfo = maidata . Levels [ id ] ;
153294 var bigTouch = id is 2 or 3 ;
154295 var isUtage = IsUtageFromLevelString ( chartInfo . Level ) ;
155296 var ma2 = SimaiToMa2 ( chartInfo . Inote , maidata . ClockCount , bigTouch , isUtage ) ;
156- var outPath = Path . Combine ( outputDir , $ "lv_{ id } .ma2") ;
157297 File . WriteAllText ( outPath , ma2 , new UTF8Encoding ( encoderShouldEmitUTF8Identifier : false ) ) ;
158298 }
159299 }
160300
161- private static void ConvertPlainSimai ( string text , string outputDir , HashSet < int > ? levelFilter )
301+ private static void ConvertPlainSimai ( string text , string outputDir , HashSet < int > ? levelFilter , string inputPath )
162302 {
163303 if ( levelFilter != null )
164304 throw new ArgumentException ( "纯 simai 单谱(非 maidata)不能使用 -l / --levels。" ) ;
165305
166306 const int outputLevel = 0 ;
167- var ma2 = SimaiToMa2 ( text ) ;
168307 var outPath = Path . Combine ( outputDir , $ "lv_{ outputLevel } .ma2") ;
308+ Console . WriteLine ( $ "Simai → MA2: { inputPath } (lv${ outputLevel } ) → { outPath } ") ;
309+ var ma2 = SimaiToMa2 ( text ) ;
169310 File . WriteAllText ( outPath , ma2 , new UTF8Encoding ( encoderShouldEmitUTF8Identifier : false ) ) ;
170311 }
171312
172313 private static string SimaiToMa2 ( string inote , int clockCount = 4 , bool bigTouch = false , bool isUtage = false )
173314 {
174315 var ( chart , parseAlerts ) = new SimaiParser ( bigTouch , isUtage , clockCount ) . Parse ( inote ) ;
316+ PrintAlerts ( parseAlerts ) ;
175317 var ( ma2 , genAlerts ) = new MA2Generator ( ) . Generate ( chart ) ;
176- var combined = new List < Alert > ( parseAlerts . Count + genAlerts . Count ) ;
177- combined . AddRange ( parseAlerts ) ;
178- combined . AddRange ( genAlerts ) ;
179- PrintAlerts ( combined ) ;
318+ PrintAlerts ( genAlerts ) ;
180319 return ma2 ;
181320 }
182321}
0 commit comments