88namespace MuConvert . chu ;
99
1010/**
11- * UGC 格式解析器(UMIGURI 格式,@TICKS=480 tick/拍)。
12- * @HEADER 标签 + #measure'tick:code 音符格式。
11+ * UMIGURI语法文档: https://gist.github.com/inonote/5c01e73781cab17765a1d93641d52298
1312 */
1413public class UgcParser : BaseChuParser
1514{
@@ -23,7 +22,6 @@ public class UgcParser: BaseChuParser
2322 [ "DC" ] = "ADW" ,
2423 [ "DR" ] = "ADR" ,
2524 [ "DL" ] = "ADL" ,
26- [ "HD" ] = "AHD" ,
2725 } ;
2826
2927 private static readonly Dictionary < string , string > ChrExtras = new ( )
@@ -273,7 +271,7 @@ private static int ParseNoteLine(string[] lines, int idx, ChuChart chart, List<A
273271 Time = measure + new Rational ( tick , RSL ) ,
274272 } ;
275273
276- var typeChar = char . ToLowerInvariant ( code [ 0 ] ) ;
274+ var typeChar = code [ 0 ] ;
277275
278276 switch ( typeChar )
279277 {
@@ -285,17 +283,29 @@ private static int ParseNoteLine(string[] lines, int idx, ChuChart chart, List<A
285283 break ;
286284
287285 case 'h' :
288- idx = ParseHoldNote ( lines , idx , code , note , alerts , chart ) ;
286+ idx = ParseHoldNote ( false , lines , idx , code , note , alerts , chart ) ;
287+ break ;
288+ case 'H' : // Air Hold
289+ idx = ParseHoldNote ( true , lines , idx , code , note , alerts , chart ) ;
289290 break ;
290291
291292 case 's' :
292- idx = ParseSlideNote ( lines , idx , code , note , alerts , chart ) ;
293+ idx = ParseSlideNote ( false , lines , idx , code , note , alerts , chart ) ;
293294 note = null ; // ParseSlideNote中,会自己构造note并自己添加进chart。因此这里默认的统一note不应被添加进chart。
294295 break ;
296+ case 'S' : // Air Slide
297+ idx = ParseSlideNote ( true , lines , idx , code , note , alerts , chart ) ;
298+ note = null ;
299+ break ;
295300
296301 case 'a' :
297302 ParseAirNote ( code , note , alerts , lineNum , chart ) ;
298303 break ;
304+ case 'C' : // Air Crush
305+ case 'T' : // 暂时不确定这是什么,只出现在v7以前版本中,v8开始已经没有了
306+ idx = ParseAirCrushNote ( lines , idx , code , note , alerts , chart ) ;
307+ note = null ;
308+ break ;
299309
300310 case 'f' :
301311 note . Type = "FLK" ;
@@ -334,54 +344,70 @@ private static void ParseTapNote(string code, ChuNote note, List<Alert> alerts,
334344 }
335345 }
336346
337- private static int ParseHoldNote ( string [ ] lines , int idx , string code , ChuNote note , List < Alert > alerts , ChuChart chart )
347+ private static int ParseHoldNote ( bool isAirHold , string [ ] lines , int idx , string code , ChuNote note , List < Alert > alerts , ChuChart chart )
338348 {
339- note . Type = "HLD" ;
349+ note . Type = isAirHold ? "AHD" : "HLD" ;
340350 ParseCellWidth ( code , 1 , note , alerts , idx + 1 , chart ) ;
341351
352+ if ( isAirHold )
353+ {
354+ // 解析颜色数据。目前只解析、不使用。
355+ _ = code . Last ( ) ; // var colorChar 颜色标记 N/I
356+ }
357+
342358 bool foundFirst = false ;
343359 while ( idx + 1 < lines . Length )
344360 {
345361 var nextLine = lines [ idx + 1 ] . Trim ( ) ;
346- if ( ! TryParseFollowerLine ( nextLine , out _ , out var duration , out _ , out _ ) )
362+ if ( ! TryParseFollowerLine ( nextLine , out var marker , out var duration , out _ , out _ , out _ , false ) )
347363 {
348364 if ( nextLine . StartsWith ( '\' ' ) || nextLine . StartsWith ( '@' ) ) { idx ++ ; continue ; }
349365 break ;
350366 }
351367
352368 note . Duration += new Rational ( duration , RSL ) ;
369+ if ( isAirHold && marker == "c" ) note . Type = "AHX" ; // 可能是对应于UMIGURI文档中的 AirHold的 AIR-ACTION 无し终点
353370 idx ++ ;
354371 foundFirst = true ;
355372 }
356373
357374 if ( ! foundFirst )
358- alerts . Add ( new Alert ( Warning , $ "HLD 音符缺少时长跟随行") { Line = idx + 1 , RelevantNote = FormatNoteRef ( note , chart ) } ) ;
375+ alerts . Add ( new Alert ( Warning , $ "HLD 音符缺少时长跟随行") { Line = idx + 1 , RelevantNote = lines [ idx ] } ) ;
359376 return idx ;
360377 }
361378
362- private static int ParseSlideNote ( string [ ] lines , int idx , string code , ChuNote previousNote , List < Alert > alerts , ChuChart chart )
379+ private static int ParseSlideNote ( bool isAirSlide , string [ ] lines , int idx , string code , ChuNote previousNote , List < Alert > alerts , ChuChart chart )
363380 {
364381 // 注:一开始从外面传进来的previousNote,最后并不会被添加进chart里,只是作为第一段的起点参照而已。
365382 var startTime = previousNote . Time ;
366383 ParseCellWidth ( code , 1 , previousNote , alerts , idx + 1 , chart ) ;
367384 previousNote . EndCell = previousNote . Cell ;
368385 previousNote . EndWidth = previousNote . Width ;
369386
387+ if ( isAirSlide )
388+ {
389+ // 解析高度和颜色数据。目前只解析、不使用。
390+ TryParseUgcBase36Int2 ( code . AsSpan ( 3 , code . Length - 4 ) , out _ ) ; // out var startHeight 起始的高度值
391+ _ = code . Last ( ) ; // var colorChar 颜色标记 N/I
392+ }
393+
370394 bool foundFirst = false ;
371395 while ( idx + 1 < lines . Length )
372396 { // 循环处理所有的跟随行。idx始终指向上一条已经处理完的行。
373397 var nextLine = lines [ idx + 1 ] . Trim ( ) ;
374- if ( ! TryParseFollowerLine ( nextLine , out var marker , out var duration , out var endCell , out var endWidth , true ) )
398+ if ( ! TryParseFollowerLine ( nextLine , out var marker , out var duration , out var endCell , out var endWidth , out _ , true ) )
375399 {
376400 if ( nextLine . StartsWith ( '\' ' ) || nextLine . StartsWith ( '@' ) ) { idx ++ ; continue ; }
377401 break ;
378402 }
379403
404+ var type = isAirSlide ? ( marker == "s" ? "ASD" : "ASC" ) : ( marker == "s" ? "SLD" : "SLC" ) ;
405+
380406 var segmentEnd = startTime + new Rational ( duration , RSL ) ;
381407 var note = new ChuNote
382408 {
383- Type = marker == "s" ? "SLD" : "SLC" ,
384- Time = previousNote . EndTime , Cell = previousNote . EndCell , Width = previousNote . EndWidth ,
409+ Type = type , Time = previousNote . EndTime ,
410+ Cell = previousNote . EndCell , Width = previousNote . EndWidth ,
385411 Duration = segmentEnd - previousNote . EndTime ,
386412 EndCell = endCell , EndWidth = endWidth ,
387413 Previous = foundFirst ? previousNote : null ,
@@ -393,37 +419,56 @@ private static int ParseSlideNote(string[] lines, int idx, string code, ChuNote
393419 }
394420
395421 if ( ! foundFirst )
396- alerts . Add ( new Alert ( Warning , $ "SLD 音符缺少时长跟随行") { Line = idx + 1 , RelevantNote = FormatNoteRef ( previousNote , chart ) } ) ;
422+ alerts . Add ( new Alert ( Warning , $ "SLD 音符缺少时长跟随行") { Line = idx + 1 , RelevantNote = lines [ idx ] } ) ;
397423
398424 return idx ;
399425 }
400426
401- private static bool TryParseFollowerLine ( string line , out string marker , out int duration , out int endCell , out int endWidth , bool requireEndCellWidth = false )
427+ private static bool TryParseUgcBase36Int2 ( ReadOnlySpan < char > twoChars , out int value )
402428 {
403- duration = 0 ;
429+ value = 0 ;
430+ if ( twoChars . Length == 0 ) return false ;
431+ else if ( twoChars . Length == 1 )
432+ {
433+ if ( ! TryHexCharToInt ( twoChars [ 0 ] , out value ) ) return false ;
434+ }
435+ else
436+ {
437+ if ( ! TryHexCharToInt ( twoChars [ 0 ] , out var hi ) || ! TryHexCharToInt ( twoChars [ 1 ] , out var lo ) ) return false ;
438+ value = hi * 36 + lo ;
439+ }
440+ return true ;
441+ }
442+
443+ private static bool TryParseFollowerLine ( string line , out string marker , out int endTick , out int endCell , out int endWidth , out int ? height , bool requireEndCellWidth )
444+ {
445+ endTick = 0 ;
404446 endCell = 0 ;
405447 endWidth = 1 ;
406448 marker = "" ;
449+ height = null ;
407450
408451 if ( ! line . StartsWith ( '#' ) ) return false ;
409452
410453 // support both >s (SLD) and >c (SLC) follower lines
411- int gtIdx = line . IndexOfAny ( [ '>' , ':' ] ) ;
412- if ( gtIdx < 1 ) return false ;
413- marker = line [ gtIdx + 1 ] . ToString ( ) ;
454+ int sepIdx = line . IndexOfAny ( [ '>' , ':' ] ) ;
455+ if ( sepIdx < 1 ) return false ;
456+ marker = line [ sepIdx + 1 ] . ToString ( ) ;
414457 int markerLen = 2 ;
415458
416- var durationStr = line [ 1 ..gtIdx ] ;
417- if ( ! int . TryParse ( durationStr , NumberStyles . Integer , CultureInfo . InvariantCulture , out duration ) ) return false ;
459+ var endTickStr = line [ 1 ..sepIdx ] ;
460+ if ( ! int . TryParse ( endTickStr , NumberStyles . Integer , CultureInfo . InvariantCulture , out endTick ) ) return false ;
418461
419- var afterMarker = line [ ( gtIdx + markerLen ) ..] ;
462+ var afterMarker = line [ ( sepIdx + markerLen ) ..] ;
420463 if ( afterMarker . Length >= 2 )
421464 {
422465 endCell = HexCharToInt ( afterMarker [ 0 ] ) ;
423- endWidth = WidthHexCharToInt ( afterMarker [ 1 ] ) ;
466+ endWidth = HexCharToInt ( afterMarker [ 1 ] ) ;
424467 }
425468 else if ( requireEndCellWidth ) return false ;
426469
470+ if ( afterMarker . Length > 2 && TryParseUgcBase36Int2 ( afterMarker . AsSpan ( ) [ 2 ..] , out var heightV ) ) height = heightV ;
471+
427472 return true ;
428473 }
429474
@@ -433,19 +478,18 @@ private static void ParseCellWidth(string code, int startIdx, ChuNote note, List
433478 {
434479 note . Cell = HexCharToInt ( code [ startIdx ] ) ;
435480 if ( code . Length > startIdx + 1 )
436- note . Width = WidthHexCharToInt ( code [ startIdx + 1 ] ) ;
481+ note . Width = HexCharToInt ( code [ startIdx + 1 ] ) ;
437482 else
438- alerts . Add ( new Alert ( Warning , $ "音符缺少 width: { code } ") { Line = lineNum , RelevantNote = FormatNoteRef ( note , chart ) } ) ;
483+ alerts . Add ( new Alert ( Warning , $ "音符缺少 width: { code } ") { Line = lineNum , RelevantNote = FormatNoteRef ( note , code ) } ) ;
439484 }
440485 else
441486 {
442- alerts . Add ( new Alert ( Warning , $ "音符缺少 cell 和 width: { code } ") { Line = lineNum , RelevantNote = FormatNoteRef ( note , chart ) } ) ;
487+ alerts . Add ( new Alert ( Warning , $ "音符缺少 cell 和 width: { code } ") { Line = lineNum , RelevantNote = FormatNoteRef ( note , code ) } ) ;
443488 }
444489 }
445490
446491 private static void ParseAirNote ( string code , ChuNote note , List < Alert > alerts , int lineNum , ChuChart chart )
447492 {
448- // Matches UgcGenerator: "a" + cell + width + two-letter direction + targetNote [ + "_" + airHoldDuration for AHD ]
449493 if ( code . Length < 5 )
450494 {
451495 alerts . Add ( new Alert ( Warning , $ "AIR 音符代码过短: { code } ") { Line = lineNum } ) ;
@@ -454,9 +498,7 @@ private static void ParseAirNote(string code, ChuNote note, List<Alert> alerts,
454498 }
455499
456500 ParseCellWidth ( code , 1 , note , alerts , lineNum , chart ) ;
457- var afterCellWidth = code [ 3 ..] ;
458- var underscoreIdx = afterCellWidth . IndexOf ( '_' ) ;
459- var mainPart = underscoreIdx >= 0 ? afterCellWidth [ ..underscoreIdx ] : afterCellWidth ;
501+ var mainPart = code [ 3 ..5 ] ;
460502
461503 if ( mainPart . Length < 2 )
462504 {
@@ -473,44 +515,58 @@ private static void ParseAirNote(string code, ChuNote note, List<Alert> alerts,
473515 else
474516 {
475517 note . Type = "AIR" ;
476- alerts . Add ( new Alert ( Warning , $ "未知的 AIR 方向: { dir } ") { Line = lineNum , RelevantNote = FormatNoteRef ( note , chart ) } ) ;
518+ alerts . Add ( new Alert ( Warning , $ "未知的 AIR 方向: { dir } ") { Line = lineNum , RelevantNote = FormatNoteRef ( note , code ) } ) ;
477519 }
520+ }
478521
479- if ( underscoreIdx >= 0 && note . Type == "AHD" )
480- {
481- var durStr = afterCellWidth [ ( underscoreIdx + 1 ) ..] ;
482- if ( int . TryParse ( durStr , NumberStyles . Integer , CultureInfo . InvariantCulture , out var ahdDuration ) )
483- note . Duration = new Rational ( ahdDuration , RSL ) ;
522+ private static int ParseAirCrushNote ( string [ ] lines , int idx , string code , ChuNote previousNote , List < Alert > alerts , ChuChart chart )
523+ {
524+ // TODO 尚未实现,所以先给个警告
525+ alerts . Add ( new Alert ( Warning , "当前版本尚未实现对Air-Crush(UMIGURI的':C'或':T'音符)的解析。" ) { Line = idx , RelevantNote = lines [ idx ] } ) ;
526+
527+ bool foundFirst = false ;
528+ while ( idx + 1 < lines . Length )
529+ { // 循环处理所有的跟随行。idx始终指向上一条已经处理完的行。
530+ var nextLine = lines [ idx + 1 ] . Trim ( ) ;
531+ if ( ! TryParseFollowerLine ( nextLine , out var marker , out var duration , out _ , out _ , out _ , false ) )
532+ {
533+ if ( nextLine . StartsWith ( '\' ' ) || nextLine . StartsWith ( '@' ) ) { idx ++ ; continue ; }
534+ break ;
535+ }
536+
537+ // TODO 尚未实现
538+ idx ++ ;
539+ foundFirst = true ;
484540 }
541+
542+ if ( ! foundFirst )
543+ alerts . Add ( new Alert ( Warning , $ "air-crush 音符缺少时长跟随行") { Line = idx + 1 , RelevantNote = lines [ idx ] } ) ;
544+ return idx ;
485545 }
486546
487547 private static int HexCharToInt ( char c )
488548 {
489- return c switch
490- {
491- >= '0' and <= '9' => c - '0' ,
492- >= 'A' and <= 'F' => c - 'A' + 10 ,
493- >= 'a' and <= 'f' => c - 'a' + 10 ,
494- _ => 0 ,
495- } ;
549+ if ( ! TryHexCharToInt ( c , out var result ) ) result = 0 ;
550+ return result ;
496551 }
497552
498- private static int WidthHexCharToInt ( char c )
553+ private static bool TryHexCharToInt ( char c , out int result )
499554 {
500- return c switch
555+ result = c switch
501556 {
502- >= '1 ' and <= '9' => c - '1' + 1 ,
503- >= 'A' and <= 'G ' => c - 'A' + 10 ,
504- >= 'a' and <= 'g ' => c - 'a' + 10 ,
505- _ => 1 ,
557+ >= '0 ' and <= '9' => c - '0' ,
558+ >= 'A' and <= 'Z ' => c - 'A' + 10 ,
559+ >= 'a' and <= 'z ' => c - 'a' + 10 ,
560+ _ => - 1 ,
506561 } ;
562+ return result >= 0 ;
507563 }
508564
509565 // ReSharper disable once UnusedParameter.Local
510- private static string FormatNoteRef ( ChuNote note , ChuChart chart )
566+ private static string FormatNoteRef ( ChuNote note , string code )
511567 {
512568 var ( m , o ) = Utils . BarAndTick ( note . Time , RSL ) ;
513- return $ "#{ m } '{ o } :{ note . Type } ";
569+ return $ "#{ m } '{ o } :{ code } ";
514570 }
515571}
516572
0 commit comments