@@ -11,16 +11,16 @@ public class UgcGenerator : IGenerator<ChuChart>
1111 public ( string , List < Alert > ) Generate ( ChuChart chart )
1212 {
1313 var alerts = new List < Alert > ( ) ;
14- var text = Serialize ( chart ) ;
14+ var text = Serialize ( chart , alerts ) ;
1515 return ( text , alerts ) ;
1616 }
1717
18- private static string Serialize ( ChuChart ugc )
18+ private static string Serialize ( ChuChart ugc , List < Alert > alerts )
1919 {
2020 ugc . Sort ( ) ;
2121
2222 var sb = new StringBuilder ( ) ;
23- sb . AppendLine ( "@VER\t 6 " ) ;
23+ sb . AppendLine ( "@VER\t 8 " ) ;
2424 if ( ! string . IsNullOrEmpty ( ugc . Title ) ) sb . AppendLine ( $ "@TITLE\t { ugc . Title } ") ;
2525 if ( ! string . IsNullOrEmpty ( ugc . Artist ) ) sb . AppendLine ( $ "@ARTIST\t { ugc . Artist } ") ;
2626 if ( ! string . IsNullOrEmpty ( ugc . Designer ) ) sb . AppendLine ( $ "@DESIGN\t { ugc . Designer } ") ;
@@ -51,24 +51,123 @@ private static string Serialize(ChuChart ugc)
5151 sb . AppendLine ( "@ENDHEAD" ) ;
5252 sb . AppendLine ( ) ;
5353
54+ // UGC Slide / AIR-SLIDE (v8):
55+ // - Chains (ChuNote.Previous) serialize as ONE parent line + follower lines (#OffsetTick from parent time).
56+ // - Ground slide: parent `s`, followers `>s` / `>c` + end cell/width.
57+ // - Air slide: parent `S` + cell/width + hh (base-36 ×2, C2S/UGC height units) + N/I; followers `>s`/`>c` + xw + hh.
58+ // - First segment may attach to TAP/HLD via Previous; only skip emit when Previous is another segment of the same chain.
59+ var slideChains = BuildSlideChains ( ugc . Notes ) ;
60+
5461 foreach ( var n in ugc . Notes )
5562 {
63+ if ( IsSlideChainNote ( n . Type ) && n . Previous != null && IsSlideChainNote ( n . Previous . Type ) )
64+ continue ; // 是Slide且不是第一段Slide,则应当已经被处理过了,直接跳过
65+
5666 var ( m , o ) = Utils . BarAndTick ( n . Time , RSL ) ;
57- sb . Append ( $ "#{ m } '{ o } :{ UCode ( n , RSL ) } ") ;
67+ var ucode = UCode ( n ) ;
68+ if ( ucode == "" )
69+ {
70+ alerts . Add ( new Alert ( Alert . LEVEL . Warning , $ "UGC Generator遇到了不支持的音符类型: { n . Type } ", n . Time , ( double ) ugc . ToSecond ( n . Time ) ) ) ;
71+ continue ;
72+ }
73+ sb . Append ( $ "#{ m } '{ o } :{ ucode } ") ;
5874 sb . AppendLine ( ) ;
75+
76+ if ( IsSlideChainNote ( n . Type ) )
77+ {
78+ if ( slideChains . TryGetValue ( n , out var segments ) )
79+ {
80+ var isAir = IsAirSlideType ( n . Type ) ;
81+ foreach ( var seg in segments )
82+ {
83+ var endTicks = Utils . Tick ( seg . EndTime - n . Time , RSL ) ;
84+ if ( endTicks <= 0 ) continue ;
85+ if ( isAir )
86+ sb . AppendLine ( $ "#{ endTicks } >{ SlideFollowerMarker ( seg . Type ) } { IntToHex ( seg . EndCell ) } { IntToHex ( seg . EndWidth ) } { EncodeUgcAirHeight2 ( AirSlideFollowerHeight ( seg ) ) } ") ;
87+ else
88+ sb . AppendLine ( $ "#{ endTicks } >{ SlideFollowerMarker ( seg . Type ) } { IntToHex ( seg . EndCell ) } { IntToHex ( seg . EndWidth ) } ") ;
89+ }
90+ }
91+ continue ;
92+ }
93+
5994 var durTicks = Utils . Tick ( n . Duration , RSL ) ;
60- if ( n . Type == "HLD" && durTicks > 0 )
95+ if ( n . Type is "HLD" or "HXD " && durTicks > 0 )
6196 sb . AppendLine ( $ "#{ durTicks } >s") ;
62- else if ( n . Type == "SLD" && durTicks > 0 )
63- sb . AppendLine ( $ "#{ durTicks } >s{ Hx ( n . EndCell ) } { Hw ( n . EndWidth ) } ") ;
97+ else if ( n . Type is "AHD" or "AHX" && durTicks > 0 )
98+ {
99+ var marker = ( n . Type == "AHX" ) ? 'c' : 's' ;
100+ sb . AppendLine ( $ "#{ durTicks } >{ marker } ") ;
101+ }
64102 }
65103 return sb . ToString ( ) ;
66104 }
67105
68- private static string UCode ( ChuNote n , int tpm )
106+ private static Dictionary < ChuNote , List < ChuNote > > BuildSlideChains ( List < ChuNote > notes )
107+ {
108+ var chains = new Dictionary < ChuNote , List < ChuNote > > ( ) ;
109+ foreach ( var n in notes )
110+ {
111+ if ( ! IsSlideChainNote ( n . Type ) ) continue ;
112+ var head = GetSlideHead ( n ) ;
113+ if ( ! chains . TryGetValue ( head , out var list ) )
114+ chains [ head ] = list = [ ] ;
115+ list . Add ( n ) ;
116+ }
117+
118+ // Order segments by their end time so follower ticks are increasing.
119+ foreach ( var ( _, segs ) in chains )
120+ {
121+ segs . Sort ( ( a , b ) =>
122+ {
123+ var t = a . EndTime . CompareTo ( b . EndTime ) ;
124+ if ( t != 0 ) return t ;
125+ // stable-ish tie-breakers
126+ t = a . Time . CompareTo ( b . Time ) ;
127+ if ( t != 0 ) return t ;
128+ t = string . CompareOrdinal ( a . Type , b . Type ) ;
129+ if ( t != 0 ) return t ;
130+ return 0 ;
131+ } ) ;
132+ }
133+
134+ // For a valid chain, follower ticks should be strictly increasing; if the chart has
135+ // degenerate segments, later code simply skips non-positive offsets.
136+ return chains ;
137+ }
138+
139+ private static ChuNote GetSlideHead ( ChuNote n )
140+ {
141+ var cur = n ;
142+ while ( cur . Previous != null && IsSlideChainNote ( cur . Previous . Type ) )
143+ cur = cur . Previous ;
144+ return cur ;
145+ }
146+
147+ private static bool IsSlideType ( string t ) => t is "SLD" or "SLC" or "SXD" or "SXC" ;
148+ private static bool IsAirSlideType ( string t ) => t is "ASD" or "ASC" ;
149+ private static bool IsSlideChainNote ( string t ) => IsSlideType ( t ) || IsAirSlideType ( t ) ;
150+ private static char SlideFollowerMarker ( string t ) => t is "SLC" or "SXC" or "ASC" ? 'c' : 's' ;
151+
152+ /// <summary> C2S col.6 / follower height: integer stored as two base-36 digits (Umiguri v8 AIR-SLIDE). </summary>
153+ private static string EncodeUgcAirHeight2 ( int value )
154+ {
155+ var v = Math . Clamp ( value * 10 , 0 , 35 * 36 + 35 ) ;
156+ var hi = v / 36 ;
157+ var lo = v % 36 ;
158+ return $ "{ IntToHex ( hi ) } { IntToHex ( lo ) } ";
159+ }
160+
161+ private static int AirSlideParentStartHeight ( ChuNote head ) => 8 ; // TODO 现在暂时写死,之后应该改成从ExtraData等地方读取
162+ private static int AirSlideFollowerHeight ( ChuNote seg ) => 8 ; // TODO 现在暂时写死,之后应该改成从ExtraData等地方读取
163+
164+ private static Dictionary < string , string > AirDirections = new ( )
69165 {
70- string c = Hx ( n . Cell ) , w = Hw ( n . Width ) ;
71- var durTicks = Utils . Tick ( n . Duration , tpm ) ;
166+ [ "AIR" ] = "UC" , [ "AUR" ] = "UR" , [ "AUL" ] = "UL" , [ "ADW" ] = "DC" , [ "ADR" ] = "DR" , [ "ADL" ] = "DL" ,
167+ } ;
168+ private static string UCode ( ChuNote n )
169+ {
170+ string c = IntToHex ( n . Cell ) , w = IntToHex ( n . Width ) ;
72171 var targetNote = string . IsNullOrEmpty ( n . TargetNote ) ? "N" : n . TargetNote ;
73172 return n . Type switch
74173 {
@@ -79,17 +178,21 @@ private static string UCode(ChuNote n, int tpm)
79178 "SLC" or "SXC" => $ "s{ c } { w } ",
80179 "FLK" => $ "f{ c } { w } A",
81180 "MNE" => $ "d{ c } { w } ",
82- "AIR" => $ "a{ c } { w } UC{ targetNote } ",
83- "AUR" => $ "a{ c } { w } UR{ targetNote } ",
84- "AUL" => $ "a{ c } { w } UL{ targetNote } ",
85- "AHD" or "AHX" => $ "a{ c } { w } HD{ targetNote } _{ durTicks } ",
86- "ADW" => $ "a{ c } { w } DC{ targetNote } ",
87- "ADR" => $ "a{ c } { w } DR{ targetNote } ",
88- "ADL" => $ "a{ c } { w } DL{ targetNote } ",
89- _ => $ "t{ c } { w } "
181+ // AIR-SLIDE (v8): #BarTick:S x w hh c
182+ "ASD" or "ASC" => $ "S{ c } { w } { EncodeUgcAirHeight2 ( AirSlideParentStartHeight ( n ) ) } { AirHoldColorSuffix ( n ) } ",
183+ "AIR" or "AUR" or "AUL" or "ADW" or "ADR" or "ADL" => $ "a{ c } { w } { AirDirections [ n . Type ] } { targetNote } { AirHoldColorSuffix ( n ) } ",
184+ // AIR-HOLD (v8): #BarTick:H x w c + 子行 #OffsetTick:s / :c(见 Umiguri Chart v8 doc)
185+ "AHD" or "AHX" => $ "H{ c } { w } { AirHoldColorSuffix ( n ) } ",
186+ _ => ""
90187 } ;
91188 }
92189
93- private static string Hx ( int v ) => "0123456789ABCDEF" [ Math . Clamp ( v , 0 , 15 ) ] . ToString ( ) ;
94- private static string Hw ( int v ) => "123456789ABCDEFG" [ Math . Clamp ( v - 1 , 0 , 15 ) ] . ToString ( ) ;
190+ private static string IntToHex ( int v ) => "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" [ Math . Clamp ( v , 0 , 35 ) ] . ToString ( ) ;
191+
192+ private static readonly Dictionary < string , string > AirColor = new ( )
193+ {
194+ [ "DEF" ] = "N" ,
195+ [ "I" ] = "I" , // TODO 搞清楚UGC里的'I'颜色,在C2S里,对应的字符串是什么
196+ } ;
197+ private static string AirHoldColorSuffix ( ChuNote n ) => AirColor . GetValueOrDefault ( n . Tag , "N" ) ;
95198}
0 commit comments