66
77namespace MuConvert . generator ;
88
9- record MA2Line ( string Name , int Bar , int Tick , int Key , string Extra = "" ) ;
10-
119public class MA2Generator : IGenerator
1210{
11+ protected record MA2Line ( string Name , int Bar , int Tick , int Key , string Extra = "" )
12+ {
13+ public override string ToString ( )
14+ {
15+ var extra = ! string . IsNullOrEmpty ( Extra ) ? "\t " + Extra : "" ;
16+ return $ "{ Name } \t { Bar } \t { Tick } \t { Key } { extra } ";
17+ }
18+ } ;
19+
1320 public MA2Generator ( bool isUtage = false )
1421 {
1522 IsUtage = isUtage ;
@@ -20,8 +27,9 @@ public MA2Generator(bool isUtage = false)
2027 public int MA2Version = 105 ;
2128 public int RSL = 384 ;
2229
23- private List < MA2Line > lines = [ ] ;
24- private readonly List < Alert > alerts = [ ] ;
30+ protected Chart chart ;
31+ protected List < MA2Line > lines = [ ] ;
32+ protected readonly List < Alert > alerts = [ ] ;
2533
2634 private string headTemplate = @"VERSION 0.00.00 {0}
2735FES_MODE {1}
@@ -36,7 +44,7 @@ GENERATED_BY MuConvert v{8}
3644
3745 private Rational __1_384 = new ( 1 , 384 ) ;
3846
39- private ( decimal , decimal , decimal , decimal ) bpmStats ( Chart chart )
47+ private ( decimal , decimal , decimal , decimal ) bpmStats ( )
4048 {
4149 var bpms = chart . BpmList . Select ( x => x . Bpm ) . ToList ( ) ;
4250 var max = bpms . Max ( ) ;
@@ -55,14 +63,19 @@ GENERATED_BY MuConvert v{8}
5563 }
5664
5765 // 持续时间/等待时间,使用"总tick数"(可超过1小节),不是小节内tick
58- private int T ( Rational r , int offset = 0 )
66+ protected int T ( Rational r , int offset = 0 )
5967 {
6068 var result = ( int ) Math . Round ( ( double ) ( r * RSL ) ) ;
6169 if ( offset != 0 ) result = Math . Max ( result + offset , result > 0 ? 1 : 0 ) ;
6270 return result ;
6371 }
6472
65- private void AddTap ( Tap tap , int bar , int tick )
73+ protected void Warn ( string description , Note note , MA2Line ? ma2Line = null )
74+ {
75+ alerts . Add ( new Alert ( Warning , description , ( chart , note . Time ) , lines . Count + 1 , ma2Line ? . ToString ( ) ) ) ;
76+ }
77+
78+ protected virtual MA2Line ? AddTap ( Tap tap , int bar , int tick )
6679 {
6780 var prefix = "NM" ;
6881 if ( tap . IsBreak && tap . IsEx ) prefix = "BX" ;
@@ -76,138 +89,156 @@ private void AddTap(Tap tap, int bar, int tick)
7689 name = "HLD" ;
7790 extra = T ( hold . Duration . Bar , - hold . FalseEachIdx ) . ToString ( ) ;
7891 }
79- lines . Add ( new MA2Line ( prefix + name , bar , tick , tap . Key - 1 , extra ) ) ;
92+ return new MA2Line ( prefix + name , bar , tick , tap . Key - 1 , extra ) ;
8093 }
81-
82- public ( string , List < Alert > ) Generate ( Chart chart )
94+
95+ protected virtual List < MA2Line > AddSlide ( Slide slide , int bar , int tick )
8396 {
84- if ( lines . Count != 0 ) throw new Exception ( Locale . InstanceMultipleUsage ) ;
85- chart . Sort ( ) ;
86- StringBuilder result = new StringBuilder ( ) ;
97+ List < MA2Line > result = [ ] ;
98+ if ( slide . OwnHead != null )
99+ {
100+ var headTap = AddTap ( slide . OwnHead , bar , tick ) ;
101+ if ( headTap != null ) result . Add ( headTap ) ;
102+ }
103+
104+ // 首先很重要的一点是,详见 https://github.com/Neskol/MaiLib/issues/46#issuecomment-3301893924 ,
105+ // 官机现在对于多段星星,是会无视掉每一段分别指定的时长,把总时长加和然后全程匀速处理的。
106+ // 至少在我上述测试的版本是这样;但为了防止万一我测试错了、或者将来相关的行为改变,这里还是尊重chart原始记法、分两类处理。
107+ var totalLen = T ( slide . Duration . Bar ) ;
108+
109+ # region 把时长平均分配给所有没有显式写出时长的段
110+ List < int ? > segmentValue = [ ] ;
111+ var unassignedValue = totalLen ;
112+ var unassignedCount = 0 ;
113+ for ( int i = 0 ; i < slide . segments . Count - 1 ; i ++ )
114+ {
115+ var seg = slide . segments [ i ] ;
116+ if ( seg . Duration != null )
117+ {
118+ var t = T ( seg . Duration . Bar ) ;
119+ segmentValue . Add ( t ) ;
120+ unassignedValue -= t ;
121+ }
122+ else
123+ {
124+ segmentValue . Add ( null ) ;
125+ unassignedCount ++ ;
126+ }
127+ }
128+ unassignedCount ++ ; // 对应于最后一段
129+ var toAssignValue = unassignedValue / unassignedCount ; // 未分配的时间分配给所有未分配段,每段分配到的量
130+ # endregion
87131
88- // 文件头
89- var bpmStatistics = bpmStats ( chart ) ;
132+ int segIdx ;
133+ for ( segIdx = 0 ; segIdx < slide . segments . Count ; segIdx ++ )
134+ {
135+ var seg = slide . segments [ segIdx ] ;
136+ var len = segIdx == slide . segments . Count - 1 ?
137+ totalLen : // 对于最后一段,剩的时间全给它。以保证总长是正确的。
138+ segmentValue [ segIdx ] ?? toAssignValue ; // 除此之外,则是优先使用显式分配的时间、没有则使用平均时间
139+ totalLen -= len ;
140+ int waitTime = 0 ;
141+
142+ var prefix = "NM" ;
143+ if ( segIdx == 0 )
144+ {
145+ if ( slide . IsBreak ) prefix = "BR" ;
146+ waitTime = T ( slide . WaitTime . Bar , - slide . FalseEachIdx ) ;
147+ }
148+ else prefix = "CN" ;
149+
150+ var name = seg . Type . ToString ( ) ;
151+
152+ result . Add ( new MA2Line ( prefix + name , bar , tick , seg . StartKey - 1 ,
153+ string . Join ( "\t " , [ waitTime , len , seg . EndKey - 1 ] ) ) ) ;
154+ tick += waitTime + len ;
155+ while ( tick >= RSL ) { tick -= RSL ; bar ++ ; }
156+ }
157+
158+ return result ;
159+ }
160+
161+ protected virtual MA2Line ? AddTouch ( Touch touch , int bar , int tick )
162+ {
163+ const string prefix = "NM" ; // touch目前只有normal的
164+ var name = "TTP" ;
165+ List < string > extras = [ ] ;
166+ if ( touch is TouchHold th )
167+ {
168+ name = "THO" ;
169+ extras . Add ( T ( th . Duration . Bar , - th . FalseEachIdx ) . ToString ( ) ) ;
170+ }
171+
172+ var area = touch . TouchArea [ 0 ] ;
173+ var key = area != 'C' ? touch . Key - 1 : 0 ; // 目前,官机还不支持C1和C2分别写touch
174+ extras . Add ( area . ToString ( ) ) ;
175+ extras . Add ( touch . IsFirework ? "1" : "0" ) ;
176+ extras . Add ( touch . TouchSize ) ;
177+
178+ return new MA2Line ( prefix + name , bar , tick , key , string . Join ( "\t " , extras ) ) ;
179+ }
180+
181+ // 生成文件头
182+ protected void GenerateFileHead ( StringBuilder result )
183+ {
184+ var bpmStatistics = bpmStats ( ) ;
90185 string head = string . Format ( headTemplate ,
91186 $ "{ MA2Version / 100 } .{ MA2Version % 100 : D2} .00", IsUtage ? 1 : 0 ,
92187 bpmStatistics . Item1 , bpmStatistics . Item2 , bpmStatistics . Item3 , bpmStatistics . Item4 ,
93188 RSL , RSL / 4 * chart . ClockCount , Utils . AppVersion ) ;
94189 result . Append ( head ) ;
95-
96- // bpm段
190+ }
191+
192+ // 生成BPM段
193+ protected void GenerateBPM ( StringBuilder result )
194+ {
97195 foreach ( var bpm in chart . BpmList )
98196 {
99197 var ( bar , tick ) = BT ( bpm . Time ) ;
100198 result . AppendLine ( $ "BPM\t { bar } \t { tick } \t { bpm . Bpm : F3} ") ;
101199 }
102200 result . AppendLine ( $ "MET\t 0\t 0\t 4\t { chart . ClockCount } ") ;
103201 result . AppendLine ( ) ;
104-
105- // 主体:音符段
106- // 由于fes星星涉及一个重排序的问题,同时也为了后面统计方便,先把note放进lines数组中最后一块写入,而不是直接写入文件
202+ }
203+
204+ // 生成主体音符段
205+ protected void GenerateNotes ( StringBuilder result )
206+ {
207+ // 由于fes星星涉及一个重排序的问题,同时也为了后面统计方便,我们先调用GenerateMA2Lines、把音符转为适合直接写入的表示并放进lines数组中,最后再一块写入StringBuilder。
107208 for ( int noteIdx = 0 ; noteIdx < chart . Notes . Count ; noteIdx ++ )
108209 {
109210 var note = chart . Notes [ noteIdx ] ;
110211 if ( noteIdx > 0 )
111212 {
112213 var distToPrev = note . Time - chart . Notes [ noteIdx - 1 ] . Time ;
113- if ( distToPrev > 0 && distToPrev < __1_384 )
114- {
115- alerts . Add ( new Alert ( Warning , Locale . NoteTooNear , ( chart , note . Time ) ) ) ;
116- }
214+ if ( distToPrev > 0 && distToPrev < __1_384 ) Warn ( Locale . NoteTooNear , note ) ;
117215 }
118216
119217 var ( bar , tick ) = BT ( note . Time , note . FalseEachIdx ) ;
120218 if ( note is Tap tap )
121219 {
122- AddTap ( tap , bar , tick ) ;
220+ var l = AddTap ( tap , bar , tick ) ;
221+ if ( l != null ) lines . Add ( l ) ;
123222 }
124223 else if ( note is Touch touch )
125224 {
126- const string prefix = "NM" ; // touch目前只有normal的
127- var name = "TTP" ;
128- List < string > extras = [ ] ;
129- if ( note is TouchHold th )
130- {
131- name = "THO" ;
132- extras . Add ( T ( th . Duration . Bar , - th . FalseEachIdx ) . ToString ( ) ) ;
133- }
134-
135- var area = touch . TouchArea [ 0 ] ;
136- var key = area != 'C' ? touch . Key - 1 : 0 ; // 目前,官机还不支持C1和C2分别写touch
137- extras . Add ( area . ToString ( ) ) ;
138- extras . Add ( touch . IsFirework ? "1" : "0" ) ;
139- extras . Add ( touch . TouchSize ) ;
140-
141- lines . Add ( new MA2Line ( prefix + name , bar , tick , key , string . Join ( "\t " , extras ) ) ) ;
225+ var l = AddTouch ( touch , bar , tick ) ;
226+ if ( l != null ) lines . Add ( l ) ;
142227 }
143228 else if ( note is Slide slide )
144229 {
145- if ( slide . OwnHead != null ) AddTap ( slide . OwnHead , bar , tick ) ;
146-
147- // 首先很重要的一点是,详见 https://github.com/Neskol/MaiLib/issues/46#issuecomment-3301893924 ,
148- // 官机现在对于多段星星,是会无视掉每一段分别指定的时长,把总时长加和然后全程匀速处理的。
149- // 至少在我上述测试的版本是这样;但为了防止万一我测试错了、或者将来相关的行为改变,这里还是尊重chart原始记法、分两类处理。
150- var totalLen = T ( slide . Duration . Bar ) ;
151-
152- # region 把时长平均分配给所有没有显式写出时长的段
153- List < int ? > segmentValue = [ ] ;
154- var unassignedValue = totalLen ;
155- var unassignedCount = 0 ;
156- for ( int i = 0 ; i < slide . segments . Count - 1 ; i ++ )
157- {
158- var seg = slide . segments [ i ] ;
159- if ( seg . Duration != null )
160- {
161- var t = T ( seg . Duration . Bar ) ;
162- segmentValue . Add ( t ) ;
163- unassignedValue -= t ;
164- }
165- else
166- {
167- segmentValue . Add ( null ) ;
168- unassignedCount ++ ;
169- }
170- }
171- unassignedCount ++ ; // 对应于最后一段
172- var toAssignValue = unassignedValue / unassignedCount ; // 未分配的时间分配给所有未分配段,每段分配到的量
173- # endregion
174-
175- int segIdx ;
176- for ( segIdx = 0 ; segIdx < slide . segments . Count ; segIdx ++ )
177- {
178- var seg = slide . segments [ segIdx ] ;
179- var len = segIdx == slide . segments . Count - 1 ?
180- totalLen : // 对于最后一段,剩的时间全给它。以保证总长是正确的。
181- segmentValue [ segIdx ] ?? toAssignValue ; // 除此之外,则是优先使用显式分配的时间、没有则使用平均时间
182- totalLen -= len ;
183- int waitTime = 0 ;
184-
185- var prefix = "NM" ;
186- if ( segIdx == 0 )
187- {
188- if ( slide . IsBreak ) prefix = "BR" ;
189- waitTime = T ( slide . WaitTime . Bar , - slide . FalseEachIdx ) ;
190- }
191- else prefix = "CN" ;
192-
193- var name = seg . Type . ToString ( ) ;
194-
195- lines . Add ( new MA2Line ( prefix + name , bar , tick , seg . StartKey - 1 ,
196- string . Join ( "\t " , [ waitTime , len , seg . EndKey - 1 ] ) ) ) ;
197- tick += waitTime + len ;
198- while ( tick >= RSL ) { tick -= RSL ; bar ++ ; }
199- }
230+ var ls = AddSlide ( slide , bar , tick ) ;
231+ foreach ( var l in ls ) lines . Add ( l ) ;
200232 }
201233 }
202-
234+
203235 lines = lines . OrderBy ( x => x . Bar * RSL + x . Tick ) . ToList ( ) ;
204- foreach ( var l in lines )
205- {
206- var extra = ! string . IsNullOrEmpty ( l . Extra ) ? "\t " + l . Extra : "" ;
207- result . AppendLine ( $ "{ l . Name } \t { l . Bar } \t { l . Tick } \t { l . Key } { extra } ") ;
208- }
236+ foreach ( var l in lines ) result . AppendLine ( l . ToString ( ) ) ;
209237 result . AppendLine ( ) ;
210-
238+ }
239+
240+ protected void GenerateStatistics ( StringBuilder result )
241+ {
211242 // 统计段
212243 var stats = chart . Statistics ;
213244 foreach ( var ( k , v ) in statsNameConversion ( ) )
@@ -250,11 +281,25 @@ private void AddTap(Tap tap, int bar, int tick)
250281 result . AppendLine ( $ "TTM_SCR_S\t { Math . Ceiling ( score_sss * 0.97 / 50 ) * 50 } ") ;
251282 result . AppendLine ( $ "TTM_SCR_SS\t { score_sss } ") ;
252283 result . AppendLine ( $ "TTM_RAT_ACV\t { ( long ) theoryScore * 10000 / score_sss } ") ; // 用long避免溢出
284+ result . AppendLine ( ) ;
285+ }
286+
287+ public ( string , List < Alert > ) Generate ( Chart _chart )
288+ {
289+ if ( chart != null ) throw new Exception ( Locale . InstanceMultipleUsage ) ;
290+ chart = _chart ;
291+ chart . Sort ( ) ;
292+ StringBuilder result = new StringBuilder ( ) ;
293+
294+ GenerateFileHead ( result ) ;
295+ GenerateBPM ( result ) ;
296+ GenerateNotes ( result ) ;
297+ GenerateStatistics ( result ) ;
253298
254299 return ( result . ToString ( ) , alerts ) ;
255300 }
256301
257- private Dictionary < string , string > statsNameConversion ( ) => new ( )
302+ protected virtual Dictionary < string , string > statsNameConversion ( ) => new ( )
258303 {
259304 [ "TAP" ] = "NMTAP" , [ "BRK" ] = "BRTAP" , [ "XTP" ] = "EXTAP" , [ "BXX" ] = "BXTAP" ,
260305 [ "HLD" ] = "NMHLD" , [ "XHO" ] = "EXHLD" , [ "BHO" ] = "BRHLD" , [ "BXH" ] = "BXHLD" ,
0 commit comments