@@ -42,6 +42,9 @@ public class UgcParser : IParser<UgcChart>
4242 var line = lines [ i ] ;
4343 if ( string . IsNullOrWhiteSpace ( line ) ) continue ;
4444
45+ // UGC comment lines (starting with ')
46+ if ( line . StartsWith ( '\' ' ) ) continue ;
47+
4548 if ( inHeader )
4649 {
4750 if ( line == "@ENDHEAD" )
@@ -175,6 +178,22 @@ private static void ParseHeaderLine(string line, UgcChart chart, List<Alert> ale
175178 }
176179 break ;
177180
181+ // silently ignored metadata tags
182+ case "@EXVER" : case "@SORT" : case "@BGM" : case "@BGMOFS" : case "@BGMPRV" :
183+ case "@JACKET" : case "@BGIMG" : case "@BGMODE" : case "@FLDCOL" : case "@FLDIMG" :
184+ case "@FLAG" : case "@ATINFO" : case "@DLURL" : case "@COPYRIGHT" : case "@LICENSE" :
185+ case "@MAINTIL" :
186+ break ;
187+
188+ case "@TIL" : case "@SPDMOD" :
189+ {
190+ var parts = value . Split ( [ '\t ' , ' ' ] , StringSplitOptions . RemoveEmptyEntries ) ;
191+ if ( parts . Length >= 2 && int . TryParse ( parts [ 0 ] , NumberStyles . Integer , CultureInfo . InvariantCulture , out var tilMeasure )
192+ && double . TryParse ( parts [ 1 ] , NumberStyles . Float , CultureInfo . InvariantCulture , out var tilMult ) )
193+ chart . SpeedEvents . Add ( ( tilMeasure , 0 , tilMult ) ) ;
194+ }
195+ break ;
196+
178197 default :
179198 alerts . Add ( new Alert ( Info , $ "未知头部标签: { tag } ") { Line = lineNum } ) ;
180199 break ;
@@ -186,6 +205,14 @@ private static int ParseNoteLine(string[] lines, int idx, UgcChart chart, List<A
186205 var line = lines [ idx ] ;
187206 var lineNum = idx + 1 ;
188207
208+ // skip comment lines and inline directives
209+ if ( line . StartsWith ( '\' ' ) || line . StartsWith ( '@' ) )
210+ return idx ;
211+
212+ // standalone follower line: silently skip (will be attached by parent or ignored)
213+ if ( line . StartsWith ( '#' ) && ! line . Contains ( ':' ) && ( line . Contains ( ">s" ) || line . Contains ( ">c" ) ) )
214+ return idx ;
215+
189216 var colonIdx = line . IndexOf ( ':' ) ;
190217 if ( colonIdx < 0 )
191218 {
@@ -257,6 +284,9 @@ private static int ParseNoteLine(string[] lines, int idx, UgcChart chart, List<A
257284 note . Extra = code [ 3 ..] ;
258285 break ;
259286
287+ case 'c' :
288+ return idx ; // Margrete Air Crush, silently skip
289+
260290 case 'd' :
261291 note . Type = "MNE" ;
262292 ParseCellWidth ( code , 1 , note , alerts , lineNum ) ;
@@ -290,6 +320,9 @@ private static int ParseHoldNote(string[] lines, int idx, string code, ChuNote n
290320 note . HoldDuration = duration ;
291321 return idx + 1 ;
292322 }
323+ // next line might be a comment or directive, not a warning
324+ if ( nextLine . StartsWith ( '\' ' ) || nextLine . StartsWith ( '@' ) )
325+ return idx ;
293326 }
294327 alerts . Add ( new Alert ( Warning , $ "HLD 音符缺少时长跟随行") { Line = idx + 1 , RelevantNote = FormatNoteRef ( note ) } ) ;
295328 return idx ;
@@ -300,25 +333,48 @@ private static int ParseSlideNote(string[] lines, int idx, string code, ChuNote
300333 note . Type = "SLD" ;
301334 ParseCellWidth ( code , 1 , note , alerts , idx + 1 ) ;
302335
303- if ( idx + 1 < lines . Length )
336+ bool foundFirst = false ;
337+ while ( idx + 1 < lines . Length )
304338 {
305339 var nextLine = lines [ idx + 1 ] . Trim ( ) ;
306- if ( TryParseFollowerLine ( nextLine , out var duration , out var endCell , out var endWidth , requireEndCellWidth : true ) )
340+ if ( ! TryParseFollowerLine ( nextLine , out var duration , out var endCell , out var endWidth ) )
307341 {
308- note . SlideDuration = duration ;
309- note . EndCell = endCell ;
310- note . EndWidth = endWidth ;
311- return idx + 1 ;
342+ if ( nextLine . StartsWith ( '\' ' ) || nextLine . StartsWith ( '@' ) ) { idx ++ ; continue ; }
343+ break ;
312344 }
313- if ( TryParseFollowerLine ( nextLine , out duration , out _ , out _ , requireEndCellWidth : false ) )
345+
346+ note . SlideDuration += duration ;
347+ note . EndCell = endCell ;
348+ note . EndWidth = endWidth ;
349+ idx ++ ;
350+ foundFirst = true ;
351+ }
352+
353+ if ( ! foundFirst )
354+ alerts . Add ( new Alert ( Warning , $ "SLD 音符缺少时长跟随行") { Line = idx + 1 , RelevantNote = FormatNoteRef ( note ) } ) ;
355+
356+ return idx ;
357+ }
358+
359+ private static bool TryParseStandaloneFollower ( string [ ] lines , int idx , UgcChart chart , List < Alert > alerts )
360+ {
361+ var line = lines [ idx ] ;
362+ if ( ! line . StartsWith ( '#' ) || ! line . Contains ( ">s" ) && ! line . Contains ( ">c" ) ) return false ;
363+
364+ if ( ! TryParseFollowerLine ( line , out var duration , out var endCell , out var endWidth ) ) return false ;
365+
366+ // find the last SLD or HLD note and attach duration
367+ for ( int i = chart . Notes . Count - 1 ; i >= 0 ; i -- )
368+ {
369+ var n = chart . Notes [ i ] ;
370+ if ( n . Type is "SLD" or "HLD" )
314371 {
315- note . SlideDuration = duration ;
316- alerts . Add ( new Alert ( Warning , $ "SLD 跟随行缺少结束位置(cell 与 width): { nextLine } " ) { Line = idx + 2 , RelevantNote = FormatNoteRef ( note ) } ) ;
317- return idx + 1 ;
372+ if ( n . Type == "SLD" ) { n . SlideDuration = duration ; n . EndCell = endCell ; n . EndWidth = endWidth ; }
373+ else { n . HoldDuration = duration ; }
374+ return true ;
318375 }
319376 }
320- alerts . Add ( new Alert ( Warning , $ "SLD 音符缺少时长跟随行") { Line = idx + 1 , RelevantNote = FormatNoteRef ( note ) } ) ;
321- return idx ;
377+ return false ;
322378 }
323379
324380 private static bool TryParseFollowerLine ( string line , out int duration , out int endCell , out int endWidth , bool requireEndCellWidth = false )
0 commit comments