1+ using System . Reflection ;
12using MuConvert . chu ;
2- using MuConvert . parser ;
3- using MuConvert . utils ;
43
54namespace MuConvert . Tests . chu ;
65
@@ -28,7 +27,67 @@ public void C2sRoundTrip()
2827 var ( chart , _) = new C2sParser ( ) . Parse ( File . ReadAllText ( C2sPath ) ) ;
2928 var ( rt , _) = new C2sGenerator ( ) . Generate ( chart ) ;
3029 var ( reparsed , _) = new C2sParser ( ) . Parse ( rt ) ;
30+
3131 Assert . Equal ( chart . Notes . Count , reparsed . Notes . Count ) ;
32+
33+ var originalSnapshots = chart . Notes
34+ . Select ( SnapshotNote )
35+ . OrderBy ( s => s )
36+ . ToArray ( ) ;
37+
38+ var reparsedSnapshots = reparsed . Notes
39+ . Select ( SnapshotNote )
40+ . OrderBy ( s => s )
41+ . ToArray ( ) ;
42+
43+ Assert . Equal ( originalSnapshots , reparsedSnapshots ) ;
44+ }
45+
46+ /// <summary>
47+ /// Builds a stable, comparable string from a note's public instance properties (name-sorted)
48+ /// so round-trip tests verify no field loss without hard-coding each property in the test.
49+ /// </summary>
50+ private static string SnapshotNote ( ChuNote note )
51+ {
52+ var props = typeof ( ChuNote ) . GetProperties ( BindingFlags . Instance | BindingFlags . Public ) ;
53+ var parts = props
54+ . OrderBy ( p => p . Name )
55+ . Select ( p => $ "{ p . Name } ={ p . GetValue ( note ) } ") ;
56+ return string . Join ( "|" , parts ) ;
57+ }
58+
59+ /// <summary>
60+ /// Same tick scaling as <see cref="C2sGenerator"/> when converting UGC → C2S (384 ticks per measure).
61+ /// </summary>
62+ private static ChuNote UgcNoteScaledToC2sTicks ( ChuNote n , int ticksPerBeat )
63+ {
64+ const int c2sResolution = 384 ;
65+ int scaleDown ( int v ) => ( int ) ( ( long ) v * ( c2sResolution / 4 ) / ticksPerBeat ) ;
66+ return new ChuNote
67+ {
68+ Type = n . Type , Measure = n . Measure , Offset = scaleDown ( n . Offset ) ,
69+ Cell = n . Cell , Width = n . Width ,
70+ HoldDuration = scaleDown ( n . HoldDuration ) , SlideDuration = scaleDown ( n . SlideDuration ) ,
71+ EndCell = n . EndCell , EndWidth = n . EndWidth ,
72+ Extra = n . Extra , TargetNote = n . TargetNote , AirHoldDuration = scaleDown ( n . AirHoldDuration ) ,
73+ StartHeight = n . StartHeight , TargetHeight = n . TargetHeight , NoteColor = n . NoteColor ,
74+ } ;
75+ }
76+
77+ /// <summary>
78+ /// Compares UGC-side notes to C2S-side notes in C2S tick space (384): snapshots of
79+ /// <see cref="UgcNoteScaledToC2sTicks"/> for each <paramref name="ugc"/> note vs snapshots of <paramref name="c2s"/> notes.
80+ /// Use with <c>UgcToC2sViaGenerator</c> (source UGC, C2S from generate+parse) or <c>C2sToUgcViaGenerator</c> (UGC from generate+parse, source C2S).
81+ /// </summary>
82+ private static void AssertUgcNotesEquivalentToReparsedC2s ( UgcChart ugc , C2sChart c2s , bool isUgcReference )
83+ {
84+ var ugcSnaps = ugc . Notes
85+ . Select ( n => SnapshotNote ( UgcNoteScaledToC2sTicks ( n , ugc . TicksPerBeat ) ) )
86+ . OrderBy ( s => s )
87+ . ToArray ( ) ;
88+ var c2sSnaps = c2s . Notes . Select ( SnapshotNote ) . OrderBy ( s => s ) . ToArray ( ) ;
89+ if ( isUgcReference ) Assert . Equal ( ugcSnaps , c2sSnaps ) ;
90+ else Assert . Equal ( c2sSnaps , ugcSnaps ) ;
3291 }
3392
3493 [ Fact ]
@@ -45,18 +104,32 @@ public void UgcToC2sViaGenerator()
45104 {
46105 if ( ! File . Exists ( UgcPath ) ) throw new SkipException ( $ "Missing: { UgcPath } ") ;
47106 var ( ugc , _) = new UgcParser ( ) . Parse ( File . ReadAllText ( UgcPath ) ) ;
107+ Assert . NotEmpty ( ugc . Notes ) ;
108+
48109 var ( c2sText , _) = new C2sGenerator ( ) . Generate ( ugc ) ;
49110 Assert . Contains ( "VERSION" , c2sText ) ;
50111 Assert . Contains ( "TAP\t " , c2sText ) ;
112+
113+ // 再把转出来的c2s,parse回去,比较是否和一开始的ugc等价(注意不是文本 round-trip,而是 IR 等价,允许字段重排但不允许信息丢失)
114+ var ( c2sChart , _) = new C2sParser ( ) . Parse ( c2sText ) ;
115+ Assert . NotEmpty ( c2sChart . Notes ) ;
116+ AssertUgcNotesEquivalentToReparsedC2s ( ugc , c2sChart , true ) ;
51117 }
52118
53119 [ Fact ]
54120 public void C2sToUgcViaGenerator ( )
55121 {
56122 if ( ! File . Exists ( C2sPath ) ) throw new SkipException ( $ "Missing: { C2sPath } ") ;
57123 var ( c2s , _) = new C2sParser ( ) . Parse ( File . ReadAllText ( C2sPath ) ) ;
124+ Assert . NotEmpty ( c2s . Notes ) ;
125+
58126 var ( ugcText , _) = new UgcGenerator ( ) . Generate ( c2s ) ;
59127 Assert . Contains ( "@VER" , ugcText ) ;
60128 Assert . Contains ( "#5'0" , ugcText ) ;
129+
130+ // 再把转出来的ugc,parse回去,比较是否和一开始的c2s等价
131+ var ( ugcReparsed , _) = new UgcParser ( ) . Parse ( ugcText ) ;
132+ Assert . NotEmpty ( ugcReparsed . Notes ) ;
133+ AssertUgcNotesEquivalentToReparsedC2s ( ugcReparsed , c2s , false ) ;
61134 }
62135}
0 commit comments