@@ -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 ;
@@ -321,6 +354,27 @@ private static int ParseSlideNote(string[] lines, int idx, string code, ChuNote
321354 return idx ;
322355 }
323356
357+ private static bool TryParseStandaloneFollower ( string [ ] lines , int idx , UgcChart chart , List < Alert > alerts )
358+ {
359+ var line = lines [ idx ] ;
360+ if ( ! line . StartsWith ( '#' ) || ! line . Contains ( ">s" ) && ! line . Contains ( ">c" ) ) return false ;
361+
362+ if ( ! TryParseFollowerLine ( line , out var duration , out var endCell , out var endWidth ) ) return false ;
363+
364+ // find the last SLD or HLD note and attach duration
365+ for ( int i = chart . Notes . Count - 1 ; i >= 0 ; i -- )
366+ {
367+ var n = chart . Notes [ i ] ;
368+ if ( n . Type is "SLD" or "HLD" )
369+ {
370+ if ( n . Type == "SLD" ) { n . SlideDuration = duration ; n . EndCell = endCell ; n . EndWidth = endWidth ; }
371+ else { n . HoldDuration = duration ; }
372+ return true ;
373+ }
374+ }
375+ return false ;
376+ }
377+
324378 private static bool TryParseFollowerLine ( string line , out int duration , out int endCell , out int endWidth , bool requireEndCellWidth = false )
325379 {
326380 duration = 0 ;
0 commit comments