1- // See https://aka.ms/new-console-template for more information
2-
1+ using System . CommandLine ;
2+ using System . Text ;
3+ using MuConvert . generator ;
4+ using MuConvert . maidata ;
5+ using MuConvert . parser . simai ;
36using MuConvert . utils ;
47
5- Console . WriteLine ( "Hello, World!" ) ;
6- Console . WriteLine ( Utils . AppVersion ) ;
8+ namespace MuConvert ;
9+
10+ internal static class Program
11+ {
12+ private static int Main ( string [ ] args )
13+ {
14+ var root = BuildRootCommand ( ) ;
15+ try
16+ {
17+ return root . Parse ( args ) . Invoke ( ) ;
18+ }
19+ catch ( NotImplementedException ex )
20+ {
21+ Console . Error . WriteLine ( ex . Message ) ;
22+ return 1 ;
23+ }
24+ catch ( ConversionException ex )
25+ {
26+ PrintAlerts ( ex . Alerts , "转换失败:" ) ;
27+ Console . Error . WriteLine ( "转换失败!报错详见如上。您可以通过 https://github.com/MuNet-OSS/MuConvert/issues 反馈问题。" ) ;
28+ return 1 ;
29+ }
30+ catch ( Exception ex ) when ( ex is ArgumentException or IOException or UnauthorizedAccessException )
31+ {
32+ Console . Error . WriteLine ( ex . Message ) ;
33+ return 1 ;
34+ }
35+ }
36+
37+ private static Command BuildRootCommand ( )
38+ {
39+ var root = new RootCommand ( $ "MuConvert { Utils . AppVersion } — simai / maidata → MA2")
40+ {
41+ Description = "将 .txt 格式的 simai 单谱或 maidata 转为 MA2,输出与输入同目录的 lv_N.ma2。"
42+ } ;
43+
44+ var levelsOption = new Option < string ? > ( "--levels" , "-l" )
45+ {
46+ Description = "仅转换指定难度(maidata 的 inote 编号),逗号分隔;省略则全部。纯 simai 单谱不可使用本选项。" ,
47+ HelpName = "N[,N...]"
48+ } ;
49+
50+ var inputArgument = new Argument < string > ( "inputfile" )
51+ {
52+ Description = "输入 .txt(单谱 simai 或 maidata)" ,
53+ Arity = ArgumentArity . ExactlyOne
54+ } ;
55+
56+ root . Options . Add ( levelsOption ) ;
57+ root . Arguments . Add ( inputArgument ) ;
58+
59+ root . SetAction ( parseResult =>
60+ {
61+ var inputPath = parseResult . GetValue ( inputArgument )
62+ ?? throw new InvalidOperationException ( "缺少参数 inputfile。" ) ;
63+ var levelsRaw = parseResult . GetValue ( levelsOption ) ;
64+ RunConvert ( inputPath , levelsRaw ) ;
65+ } ) ;
66+
67+ return root ;
68+ }
69+
70+ private static void RunConvert ( string inputPath , string ? levelsRaw )
71+ {
72+ var levelFilter = string . IsNullOrWhiteSpace ( levelsRaw )
73+ ? null
74+ : ParseLevelList ( levelsRaw ) ;
75+
76+ var ext = Path . GetExtension ( inputPath ) ;
77+ if ( string . Equals ( ext , ".ma2" , StringComparison . OrdinalIgnoreCase ) )
78+ throw new NotImplementedException ( "从 .ma2 输入的转换尚未实现。" ) ;
79+
80+ if ( ! string . Equals ( ext , ".txt" , StringComparison . OrdinalIgnoreCase ) )
81+ throw new ArgumentException ( $ "不支持的输入扩展名「{ ext } 」。目前仅支持 .txt(simai / maidata)。") ;
82+
83+ if ( ! File . Exists ( inputPath ) )
84+ throw new ArgumentException ( $ "找不到文件: { inputPath } ") ;
85+
86+ var inputDir = Path . GetDirectoryName ( Path . GetFullPath ( inputPath ) ) ! ;
87+ var text = File . ReadAllText ( inputPath , Encoding . UTF8 ) ;
88+
89+ if ( LooksLikeMaidata ( text ) )
90+ ConvertMaidata ( text , inputDir , levelFilter ) ;
91+ else
92+ ConvertPlainSimai ( text , inputDir , levelFilter ) ;
93+ }
94+
95+ private static HashSet < int > ParseLevelList ( string s )
96+ {
97+ var parts = s . Split ( ',' , StringSplitOptions . RemoveEmptyEntries | StringSplitOptions . TrimEntries ) ;
98+ if ( parts . Length == 0 )
99+ throw new ArgumentException ( "-l / --levels 的难度列表不能为空。" ) ;
100+
101+ var set = new HashSet < int > ( ) ;
102+ foreach ( var p in parts )
103+ {
104+ if ( ! int . TryParse ( p , out var id ) || id <= 0 )
105+ throw new ArgumentException ( $ "无效的难度编号: 「{ p } 」。") ;
106+ set . Add ( id ) ;
107+ }
108+ return set ;
109+ }
110+
111+ private static bool LooksLikeMaidata ( string text ) =>
112+ text . Contains ( "&inote_" , StringComparison . Ordinal ) ;
113+
114+ /// <summary>
115+ /// lv_N 字段:空或仅含数字、点、加号 → 非宴谱;否则(含汉字等)→ 宴谱。
116+ /// </summary>
117+ private static bool IsUtageFromLevelString ( string ? level )
118+ {
119+ if ( string . IsNullOrWhiteSpace ( level ) )
120+ return false ;
121+ foreach ( var c in level . Trim ( ) )
122+ {
123+ if ( char . IsDigit ( c ) || c is '.' or '+' )
124+ continue ;
125+ return true ;
126+ }
127+ return false ;
128+ }
129+
130+ private static void PrintAlerts ( IReadOnlyList < Alert > alerts , string ? header = null )
131+ {
132+ if ( alerts . Count == 0 )
133+ return ;
134+ if ( header != null )
135+ Console . Error . WriteLine ( header ) ;
136+ foreach ( var a in alerts )
137+ Console . Error . WriteLine ( a . ToString ( ) ) ;
138+ }
139+
140+ private static void ConvertMaidata ( string text , string outputDir , HashSet < int > ? levelFilter )
141+ {
142+ var maidata = new Maidata ( text ) ;
143+ var ids = maidata . Levels . Keys . OrderBy ( k => k ) . ToList ( ) ;
144+ if ( ids . Count == 0 )
145+ throw new ArgumentException ( "maidata 中未找到任何 &inote_* 谱面。" ) ;
146+
147+ var selected = levelFilter == null
148+ ? ids
149+ : ids . Where ( id => levelFilter . Contains ( id ) ) . ToList ( ) ;
150+
151+ if ( selected . Count == 0 )
152+ throw new ArgumentException ( "-l / --levels 指定的难度在文件中均不存在。" ) ;
153+
154+ foreach ( var id in selected )
155+ {
156+ var chartInfo = maidata . Levels [ id ] ;
157+ var bigTouch = id is 2 or 3 ;
158+ var isUtage = IsUtageFromLevelString ( chartInfo . Level ) ;
159+ var ma2 = SimaiToMa2 ( chartInfo . Inote , maidata . ClockCount , bigTouch , isUtage ) ;
160+ var outPath = Path . Combine ( outputDir , $ "lv_{ id } .ma2") ;
161+ File . WriteAllText ( outPath , ma2 , new UTF8Encoding ( encoderShouldEmitUTF8Identifier : false ) ) ;
162+ }
163+ }
164+
165+ private static void ConvertPlainSimai ( string text , string outputDir , HashSet < int > ? levelFilter )
166+ {
167+ if ( levelFilter != null )
168+ throw new ArgumentException ( "纯 simai 单谱(非 maidata)不能使用 -l / --levels。" ) ;
169+
170+ const int outputLevel = 0 ;
171+ var ma2 = SimaiToMa2 ( text ) ;
172+ var outPath = Path . Combine ( outputDir , $ "lv_{ outputLevel } .ma2") ;
173+ File . WriteAllText ( outPath , ma2 , new UTF8Encoding ( encoderShouldEmitUTF8Identifier : false ) ) ;
174+ }
175+
176+ private static string SimaiToMa2 ( string inote , int clockCount = 4 , bool bigTouch = false , bool isUtage = false )
177+ {
178+ var ( chart , parseAlerts ) = new SimaiParser ( bigTouch , isUtage ) . Parse ( inote ) ;
179+ var ( ma2 , genAlerts ) = new MA2Generator ( clockCount ) . Generate ( chart ) ;
180+ var combined = new List < Alert > ( parseAlerts . Count + genAlerts . Count ) ;
181+ combined . AddRange ( parseAlerts ) ;
182+ combined . AddRange ( genAlerts ) ;
183+ PrintAlerts ( combined ) ;
184+ return ma2 ;
185+ }
186+ }
0 commit comments