Skip to content

Commit c2c0d80

Browse files
committed
fix: 多段Air-Hold(中途有air-action),未能被正确处理的问题。
关于该问题,详见 #5
1 parent d9aecd8 commit c2c0d80

3 files changed

Lines changed: 49 additions & 62 deletions

File tree

‎generator/chu/UgcGenerator.cs‎

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -102,17 +102,18 @@ private string Serialize(ChuChart ugc)
102102

103103
var notes = SortedNotesForConnectingPrevious(ugc);
104104

105-
// UGC Slide / AIR-SLIDE (v8):
105+
// UGC Slide / AIR-SLIDE / AIR-HOLD (v8):
106106
// - Chains (ChuNote.Previous) serialize as ONE parent line + follower lines (#OffsetTick from parent time).
107107
// - Ground slide: parent `s`, followers `>s` / `>c` + end cell/width.
108-
// - Air slide: parent `S` + cell/width + hh (base-36 ×2, C2S/UGC height units) + N/I; followers `>s`/`>c` + xw + hh.
108+
// - Air slide: parent `S` + cell/width + hh + N/I; followers `>s`/`>c` + xw + hh.
109+
// - Air hold: parent `H` + cell/width + color; followers `>s` / `>c` only.
109110
// - First segment may attach to TAP/HLD via Previous; only skip emit when Previous is another segment of the same chain.
110111
var slideChains = BuildSlideChains(notes);
111112

112113
foreach (var n in notes)
113114
{
114-
if (IsSlideChainNote(n.Type) && IsSlideContinueSegments(n))
115-
continue; // 是Slide且不是第一段Slide,则应当已经被处理过了,直接跳过
115+
if (IsSlideChainNote(n.Type) && IsChainContinueSegments(n))
116+
continue; // 是链式音符且不是第一段,则应当已经被处理过了,直接跳过
116117

117118
var (m, o) = Utils.BarAndTick(n.Time, RSL);
118119
var ucode = UCode(n);
@@ -128,15 +129,16 @@ private string Serialize(ChuChart ugc)
128129
{
129130
if (slideChains.TryGetValue(n, out var segments))
130131
{
131-
var isAir = IsAirSlide(n.Type);
132132
foreach (var seg in segments)
133133
{
134134
var endTicks = Utils.Tick(seg.EndTime - n.Time, RSL);
135135
if (endTicks <= 0) continue;
136-
if (isAir)
136+
if (IsAirSlide(n.Type))
137137
sb.AppendLine($"#{endTicks}>{SlideFollowerMarker(seg.Type)}{IToH36(seg.EndCell)}{IToH36(seg.EndWidth)}{EncodeAirHeight(seg.EndHeight)}");
138-
else
138+
else if (IsSlide(n.Type))
139139
sb.AppendLine($"#{endTicks}>{SlideFollowerMarker(seg.Type)}{IToH36(seg.EndCell)}{IToH36(seg.EndWidth)}");
140+
else
141+
sb.AppendLine($"#{endTicks}>{SlideFollowerMarker(seg.Type)}");
140142
}
141143
}
142144
continue;
@@ -145,11 +147,6 @@ private string Serialize(ChuChart ugc)
145147
var durTicks = Utils.Tick(n.Duration, RSL);
146148
if (n.Type is "HLD" or "HXD" && durTicks > 0)
147149
sb.AppendLine($"#{durTicks}>s");
148-
else if (n.Type is "AHD" or "AHX" && durTicks > 0)
149-
{
150-
var marker = (n.Type == "AHX") ? 'c' : 's';
151-
sb.AppendLine($"#{durTicks}>{marker}");
152-
}
153150
else if (n.Type is "ALD" && durTicks > 0)
154151
sb.AppendLine($"#{durTicks}>c{IToH36(n.EndCell)}{IToH36(n.EndWidth)}{EncodeAirHeight(n.EndHeight)}");
155152
}
@@ -192,14 +189,16 @@ private static Dictionary<ChuNote, List<ChuNote>> BuildSlideChains(List<ChuNote>
192189
private static ChuNote GetSlideHead(ChuNote n)
193190
{
194191
var cur = n;
195-
while (IsSlideContinueSegments(cur)) cur = cur.Previous!;
192+
while (IsChainContinueSegments(cur)) cur = cur.Previous!;
196193
return cur;
197194
}
198195

199-
private static bool IsSlideChainNote(string t) => IsSlide(t) || IsAirSlide(t);
200-
// 返回true表示,当前ChuNote对应的Slide Segment,是第二段之后(也就是接在别的segment之后)的segment,而不是首段segment,
201-
private static bool IsSlideContinueSegments(ChuNote n) // Air Slide的前驱只能是Air Slide,反之亦然。
202-
=> (IsSlide(n) && IsSlide(n.Previous)) || (IsAirSlide(n) && IsAirSlide(n.Previous));
196+
private static bool IsSlideChainNote(string t) => IsSlide(t) || IsAirSlide(t) || IsAirHold(t);
197+
// 返回 true 表示当前 segment 接在同类型链的上一段之后,而非首段。
198+
private static bool IsChainContinueSegments(ChuNote n)
199+
=> (IsSlide(n) && IsSlide(n.Previous))
200+
|| (IsAirSlide(n) && IsAirSlide(n.Previous))
201+
|| (IsAirHold(n) && IsAirHold(n.Previous));
203202
private static char SlideFollowerMarker(string t) => t is "SLC" or "SXC" or "ASC" ? 'c' : 's';
204203

205204
private static string EncodeAirHeight(decimal value) => IToH36(Math.Clamp((int)Math.Round(C2U_Height(value) * 10), 0, 1295)).PadLeft(2, '0');

‎parser/chu/BaseChuParser.cs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ protected static List<ChuNote> FilterPreviousCandidates(ChuNote cur, List<ChuNot
7878
result.AddRange(candidates.Where(n => IsAirSlide(n.Type)));
7979
result.AddRange(candidates.Where(n => !IsAirSlide(n.Type) && IsLegalPreviousForAir(n.Type)));
8080
}
81+
else if (IsAirHold(cur.Type))
82+
{ // Air Hold:优先匹配“上一段airhold”,其次匹配“上一段其他
83+
result.AddRange(candidates.Where(n => IsAirHold(n.Type)));
84+
result.AddRange(candidates.Where(n => !IsAirHold(n.Type) && IsLegalPreviousForAir(n.Type)));
85+
}
8186
else if (IsAir(cur.Type) || IsAirHold(cur.Type))
8287
{ // Air 系列:依附在一个“非广义Air”的音符上
8388
result.AddRange(candidates.Where(n => IsLegalPreviousForAir(n.Type)).ToList());

‎parser/chu/UgcParser.cs‎

Lines changed: 28 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -276,19 +276,11 @@ private int ParseNoteLine(string[] lines, int idx, ChuChart chart, List<Alert> a
276276
break;
277277

278278
case 'h':
279-
idx = ParseHoldNote(false, lines, idx, code, note, alerts, chart);
280-
break;
281279
case 'H': // Air Hold
282-
idx = ParseHoldNote(true, lines, idx, code, note, alerts, chart);
283-
break;
284-
285280
case 's':
286-
idx = ParseSlideNote(false, lines, idx, code, note, alerts, chart);
287-
note = null; // ParseSlideNote中,会自己构造note并自己添加进chart。因此这里默认的统一note不应被添加进chart。
288-
break;
289281
case 'S': // Air Slide
290-
idx = ParseSlideNote(true, lines, idx, code, note, alerts, chart);
291-
note = null;
282+
idx = ParseHoldOrSlideNote(typeChar, lines, idx, code, note, alerts, chart);
283+
note = null; // ParseHoldOrSlideNote中,会自己构造note并自己添加进chart。因此这里默认的统一note不应被添加进chart。
292284
break;
293285

294286
case 'a':
@@ -382,33 +374,6 @@ private void ParseHeightAndColor(ChuNote n, string str, List<Alert> alerts, int
382374
else alerts.Add(new Alert(Warning, "解析Air系列音符的高度属性失败!", n.Time, null, lineNum, FormatNoteRef(n, str)));
383375
}
384376
}
385-
386-
private int ParseHoldNote(bool isAirHold, string[] lines, int idx, string code, ChuNote note, List<Alert> alerts, ChuChart chart)
387-
{
388-
note.Type = isAirHold ? "AHD" : "HLD";
389-
ParseCellWidth(code, 1, note, alerts, idx + 1, chart);
390-
if (isAirHold) ParseHeightAndColor(note, code[3..], alerts, idx+1, "H");
391-
392-
bool foundFirst = false;
393-
while (idx + 1 < lines.Length)
394-
{
395-
var nextLine = lines[idx + 1].Trim();
396-
if (!TryParseFollowerLine(nextLine, out var marker, out var endTick, out _, out _, out _, false))
397-
{
398-
if (nextLine.StartsWith('\'') || nextLine.StartsWith('@')) { idx++; continue; }
399-
break;
400-
}
401-
402-
note.Duration = new Rational(endTick, RSL);
403-
if (isAirHold && marker == "c") note.Type = "AHX"; // 可能是对应于UMIGURI文档中的 AirHold的 AIR-ACTION 无し终点
404-
idx++;
405-
foundFirst = true;
406-
}
407-
408-
if (!foundFirst)
409-
alerts.Add(new Alert(Warning, $"HLD 音符缺少时长跟随行") { Line = idx + 1, RelevantNote = lines[idx] });
410-
return idx;
411-
}
412377

413378
// UGC中约定Air系列音符都一定紧跟在其Previous的后面。
414379
// 所以我们直接用上一个解析出的note就可以立即确定前驱了,无需再等到最后集中FillPrevious,而且等到最后集中FillPrevious时的结果也可能是错的。
@@ -427,12 +392,14 @@ private bool AddAirPreviousFromLastNote(ChuNote note, ChuChart chart)
427392
return false;
428393
}
429394

430-
private int ParseSlideNote(bool isAirSlide, string[] lines, int idx, string code, ChuNote previousNote, List<Alert> alerts, ChuChart chart)
395+
private int ParseHoldOrSlideNote(char noteType, string[] lines, int idx, string code, ChuNote previousNote, List<Alert> alerts, ChuChart chart)
431396
{
397+
bool isAir = noteType is 'S' or 'H';
398+
bool isSlide = noteType is 'S' or 's';
432399
// 注:一开始从外面传进来的previousNote,最后并不会被添加进chart里,只是作为第一段的起点参照而已。
433400
var startTime = previousNote.Time;
434401
ParseCellWidth(code, 1, previousNote, alerts, idx + 1, chart);
435-
if (isAirSlide) ParseHeightAndColor(previousNote, code[3..], alerts, idx+1, "S");
402+
if (isAir) ParseHeightAndColor(previousNote, code[3..], alerts, idx+1, noteType.ToString());
436403
previousNote.EndCell = previousNote.Cell;
437404
previousNote.EndWidth = previousNote.Width;
438405
previousNote.EndHeight = previousNote.Height;
@@ -441,26 +408,42 @@ private int ParseSlideNote(bool isAirSlide, string[] lines, int idx, string code
441408
while (idx + 1 < lines.Length)
442409
{ // 循环处理所有的跟随行。idx始终指向上一条已经处理完的行。
443410
var nextLine = lines[idx + 1].Trim();
444-
if (!TryParseFollowerLine(nextLine, out var marker, out var endTick, out var endCell, out var endWidth, out var endHeight, true))
411+
if (!TryParseFollowerLine(nextLine, out var marker, out var endTick, out var endCell, out var endWidth, out var endHeight, isSlide))
445412
{
446413
if (nextLine.StartsWith('\'') || nextLine.StartsWith('@')) { idx++; continue; }
447414
break;
448415
}
449416

450-
var type = isAirSlide ? (marker == "s" ? "ASD" : "ASC") : (marker == "s" ? "SLD" : "SLC");
417+
var type = noteType switch
418+
{
419+
's' => marker == "s" ? "SLD" : "SLC",
420+
'S' => marker == "s" ? "ASD" : "ASC",
421+
'h' => "HLD",
422+
'H' => marker == "s" ? "AHD" : "AHX",
423+
_ => throw new Exception($"未知的noteType: {noteType}"),
424+
};
425+
if (noteType == 'h' && marker == "c") alerts.Add(new Alert(Warning, $"Hold不应有c类型的跟随行", (chart, previousNote.Time), idx + 1, lines[idx]));
451426

452427
var segmentEnd = startTime + new Rational(endTick, RSL);
453428
var note = new ChuNote
454429
{
455430
Type = type, Time = previousNote.EndTime,
456-
Cell = previousNote.EndCell, Width = previousNote.EndWidth, Height = previousNote.EndHeight,
431+
Cell = previousNote.EndCell, Width = previousNote.EndWidth,
457432
Duration = segmentEnd - previousNote.EndTime, Tag = previousNote.Tag,
458-
EndCell = endCell!.Value, EndWidth = endWidth!.Value,
459-
EndHeight = endHeight != null ? U2C_Height(endHeight.Value) : previousNote.EndHeight,
460433
Previous = foundFirst ? previousNote : null,
461434
};
435+
if (isSlide)
436+
{
437+
note.EndCell = endCell!.Value;
438+
note.EndWidth = endWidth!.Value;
439+
if (isAir)
440+
{
441+
note.Height = previousNote.EndHeight;
442+
note.EndHeight = endHeight != null ? U2C_Height(endHeight.Value) : note.Height;
443+
}
444+
}
462445

463-
if (isAirSlide && !foundFirst)
446+
if (isAir && !foundFirst)
464447
{
465448
if (!AddAirPreviousFromLastNote(note, chart)) // 尝试直接从上一个note添加前驱。如果失败了报警告。
466449
alerts.Add(new Alert(Warning, $"无法找到 Air Slide 的前驱音符", (chart, note.Time), idx + 1, lines[idx]));

0 commit comments

Comments
 (0)