Skip to content

Commit fa5e32f

Browse files
committed
[!F] UGC要求Air必须紧跟在其依附的音符的后面,之前的实现没有正确实现这一点,现在进行了修复。
1 parent ab84b09 commit fa5e32f

3 files changed

Lines changed: 64 additions & 21 deletions

File tree

generator/chu/UgcGenerator.cs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,33 @@ public class UgcGenerator : IGenerator<ChuChart>
1616
return (text, alerts);
1717
}
1818

19+
/**
20+
* 对 chart.Notes 做一次稳定重排序,使得任何具有 Previous 的音符都会紧紧地出现在它的 Previous 之后,从而满足 UGC 对“Air/Air Slide应该紧跟着其依附的音符”的格式要求。
21+
*/
22+
private List<ChuNote> SortedNotesForConnectingPrevious(ChuChart chart)
23+
{
24+
// 1. 基于Previous,反向构建Next信息
25+
var nextDict = new Dictionary<ChuNote, List<ChuNote>>();
26+
foreach (var n in chart.Notes)
27+
{
28+
if (n.Previous != null) nextDict.Add(n.Previous, n);
29+
}
30+
31+
// 2. 遍历 chart.Notes,对每个 ChuNote 以 DFS 方式把它本身以及它所有 Next 子孙依次加入结果。
32+
var result = new List<ChuNote>(chart.Notes.Count);
33+
var visited = new HashSet<ChuNote>();
34+
foreach (var root in chart.Notes) Dfs(root);
35+
return result;
36+
37+
void Dfs(ChuNote n)
38+
{
39+
if (!visited.Add(n)) return;
40+
result.Add(n);
41+
if (!nextDict.TryGetValue(n, out var nexts)) return;
42+
foreach (var next in nexts) Dfs(next);
43+
}
44+
}
45+
1946
private string Serialize(ChuChart ugc, List<Alert> alerts)
2047
{
2148
ugc.Sort();
@@ -52,14 +79,16 @@ private string Serialize(ChuChart ugc, List<Alert> alerts)
5279
sb.AppendLine("@ENDHEAD");
5380
sb.AppendLine();
5481

82+
var notes = SortedNotesForConnectingPrevious(ugc);
83+
5584
// UGC Slide / AIR-SLIDE (v8):
5685
// - Chains (ChuNote.Previous) serialize as ONE parent line + follower lines (#OffsetTick from parent time).
5786
// - Ground slide: parent `s`, followers `>s` / `>c` + end cell/width.
5887
// - Air slide: parent `S` + cell/width + hh (base-36 ×2, C2S/UGC height units) + N/I; followers `>s`/`>c` + xw + hh.
5988
// - First segment may attach to TAP/HLD via Previous; only skip emit when Previous is another segment of the same chain.
60-
var slideChains = BuildSlideChains(ugc.Notes);
89+
var slideChains = BuildSlideChains(notes);
6190

62-
foreach (var n in ugc.Notes)
91+
foreach (var n in notes)
6392
{
6493
if (IsSlideChainNote(n.Type) && IsSlideContinueSegments(n))
6594
continue; // 是Slide且不是第一段Slide,则应当已经被处理过了,直接跳过

parser/chu/BaseChuParser.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ private static bool NeedsPrevious(ChuNote n)
6464
return IsSlide(n.Type) || IsAir(n.Type) || IsAirHold(n.Type) || IsAirSlide(n.Type);
6565
}
6666

67-
private static List<ChuNote> FilterPreviousCandidates(ChuNote cur, List<ChuNote> candidates)
67+
protected static List<ChuNote> FilterPreviousCandidates(ChuNote cur, List<ChuNote> candidates)
6868
{ // 注意:候选列表已满足“首尾相接”,这里仅做类型约束
6969
List<ChuNote> result = [];
7070
candidates = candidates.Where(n => n != cur).ToList(); // 自己不能成为自己的candidate,防止自环

parser/chu/UgcParser.cs

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,23 @@ private int ParseHoldNote(bool isAirHold, string[] lines, int idx, string code,
408408
alerts.Add(new Alert(Warning, $"HLD 音符缺少时长跟随行") { Line = idx + 1, RelevantNote = lines[idx] });
409409
return idx;
410410
}
411+
412+
// UGC中约定Air系列音符都一定紧跟在其Previous的后面。
413+
// 所以我们直接用上一个解析出的note就可以立即确定前驱了,无需再等到最后集中FillPrevious,而且等到最后集中FillPrevious时的结果也可能是错的。
414+
// 本函数作为一个工具函数干的就是这个事情。
415+
private bool AddAirPreviousFromLastNote(ChuNote note, ChuChart chart)
416+
{
417+
if (chart.Notes.Count > 0)
418+
{
419+
var filtered = FilterPreviousCandidates(note, [chart.Notes.Last()]); // 仅传入一个元素到FilterPreviousCandidates,因此返回结果最多一个元素
420+
if (filtered.Count > 0)
421+
{
422+
note.Previous = filtered[0];
423+
return true;
424+
}
425+
}
426+
return false;
427+
}
411428

412429
private int ParseSlideNote(bool isAirSlide, string[] lines, int idx, string code, ChuNote previousNote, List<Alert> alerts, ChuChart chart)
413430
{
@@ -441,6 +458,12 @@ private int ParseSlideNote(bool isAirSlide, string[] lines, int idx, string code
441458
EndHeight = endHeight != null ? U2C_Height(endHeight.Value) : previousNote.EndHeight,
442459
Previous = foundFirst ? previousNote : null,
443460
};
461+
462+
if (isAirSlide && !foundFirst)
463+
{
464+
if (!AddAirPreviousFromLastNote(note, chart)) // 尝试直接从上一个note添加前驱。如果失败了报警告。
465+
alerts.Add(new Alert(Warning, $"无法找到 Air Slide 的前驱音符", (chart, note.Time), idx + 1, lines[idx]));
466+
}
444467

445468
chart.Notes.Add(note);
446469
previousNote = note;
@@ -508,34 +531,25 @@ private void ParseCellWidth(string code, int startIdx, ChuNote note, List<Alert>
508531

509532
private void ParseAirNote(string code, ChuNote note, List<Alert> alerts, int lineNum, ChuChart chart)
510533
{
534+
note.Type = "AIR"; // 出错情况下的缺省值
511535
if (code.Length < 5)
512536
{
513537
alerts.Add(new Alert(Warning, $"AIR 音符代码过短: {code}") { Line = lineNum });
514-
note.Type = "AIR";
515538
return;
516539
}
517540

518541
ParseCellWidth(code, 1, note, alerts, lineNum, chart);
519542
var mainPart = code[3..];
520543

521-
if (mainPart.Length < 2)
522-
{
523-
alerts.Add(new Alert(Warning, $"AIR 音符方向代码过短: {code}") { Line = lineNum });
524-
note.Type = "AIR";
525-
return;
526-
}
527-
528-
var dir = mainPart[..2];
529-
if (U2C_AirDirections.TryGetValue(dir, out var airType))
530-
{
531-
note.Type = airType;
532-
}
533-
else
534-
{
535-
note.Type = "AIR";
536-
alerts.Add(new Alert(Warning, $"未知的 AIR 方向: {dir}") { Line = lineNum, RelevantNote = FormatNoteRef(note, code) });
537-
}
544+
// 解析方向
545+
var direction = mainPart[..2];
546+
if (U2C_AirDirections.TryGetValue(direction, out var airType)) note.Type = airType;
547+
else alerts.Add(new Alert(Warning, $"未知的 AIR 方向: {direction}") { Line = lineNum, RelevantNote = FormatNoteRef(note, code) });
548+
// 解析颜色
538549
ParseHeightAndColor(note, mainPart[2..], alerts, lineNum, "a");
550+
551+
if (!AddAirPreviousFromLastNote(note, chart)) // 尝试直接从上一个note添加前驱。如果失败了报警告。
552+
alerts.Add(new Alert(Warning, $"无法找到 Air 的前驱音符", (chart, note.Time), lineNum + 1, code));
539553
}
540554

541555
private int ParseAirCrushNote(string[] lines, int idx, string code, ChuNote note, List<Alert> alerts, ChuChart chart)

0 commit comments

Comments
 (0)