Skip to content

Commit 9c13621

Browse files
committed
[+] 在ChuNote中新增Previous字段,新增BaseChuParser和FillAllPrevious通用工具方法。以实现slide的链式关联。
1 parent fa9dbcb commit 9c13621

6 files changed

Lines changed: 140 additions & 23 deletions

File tree

chart/chu/ChuNote.cs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,40 @@ public class ChuNote: BaseNote
1616
public int Width { get; set; } = 1;
1717
/** HLD/SLD/AHD/ASD等的 持续时长 */
1818
public Rational Duration { get; set => field = value.CanonicalForm; } = 0;
19+
1920
/** SLD 终点列 */
20-
public int EndCell { get; set; }
21+
public int EndCell
22+
{
23+
get => _endCell ?? Cell;
24+
set => _endCell = value;
25+
}
2126
/** SLD 终点宽度 */
22-
public int EndWidth { get; set; } = 1;
23-
/** Air系列音符/Slide系列音符的 关联的目标音符类型 */
24-
public string TargetNote { get; set; } = "";
27+
public int EndWidth
28+
{
29+
get => _endWidth ?? Width;
30+
set => _endWidth = value;
31+
}
32+
33+
/**
34+
* 当前音符的”前驱“。对不同类型的音符,其定义不同:
35+
* - 对 Slide(SLD/SLC),是该slide对应的前一段slide。(对首段slide,该值为null)
36+
* - 对 Air(AIR/AUR/AUL/ADW/ADR/ADL),是它所依附的音符(可以是tap\hold等任何类型,应该只要不是air系列和aircrush(ALD)都行)
37+
* - 对 Air Slide(ASD/ASC):对首段slide,同Air的情况、是它所依附的音符;对第二段及之后的slide,同Slide的情况,是该slide对应的前一段slide。
38+
*
39+
* 不难分析出,在完成整个chart之后,这个属性其实可以根据完整chart的列表动态推断的。
40+
* 因此,在BaseChuParser类中提供了FillAllPrevious方法,该方法应该在所有Note被正常解析完成后调用,填充所有上述类型的音符的targetNote信息。这样就不用每个Parser都写一段相似的逻辑。
41+
*/
42+
public ChuNote? Previous;
43+
2544
/** CHR/FLK/Air系列音符可能会具有的标记(如UP、L、DEF等) */
2645
public string Tag { get; set; } = "";
2746
/** ASD/ASC/ALD上具有的、目前含义还不明确的字段,统一收集到这个里面。 */
2847
public List<int> ExtraData = [];
2948

3049
public override Rational EndTime => (Time + Duration).CanonicalForm;
50+
/** Air系列音符/Slide系列音符的 关联的目标音符类型。仅供向前兼容使用。 */
51+
public string TargetNote => Previous?.Type ?? "N";
52+
53+
private int? _endCell;
54+
private int? _endWidth;
3155
}

parser/chu/BaseChuParser.cs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using MuConvert.chu;
2+
using MuConvert.utils;
3+
4+
namespace MuConvert.parser;
5+
6+
public abstract class BaseChuParser : IParser<ChuChart>
7+
{
8+
public abstract (ChuChart, List<Alert>) Parse(string text);
9+
10+
/**
11+
* 填充所有需要 Previous 的音符(见 <see cref="ChuNote.Previous"/> 注释)。
12+
* 只会填充当前Previous没有被设置过的音符:如果某个音符的Previous不为null(在Parse过程中已经被设置过了),则会尊重Parse的决定,不会再次设置。
13+
*
14+
* 推断规则:
15+
* - 前驱音符必须满足“首尾相接”:prev.EndTime == cur.Time 且 prev.EndCell == cur.Cell 且 prev.EndWidth == cur.Width
16+
* - 再按音符类型施加额外约束(slide / air / air-slide)
17+
*
18+
* 该方法应在所有音符解析完成后调用。
19+
*
20+
* <param name="chart">谱面对象</param>
21+
* <param name="alerts">过程中产生的警告会被放进这个数组里。</param>
22+
* <param name="rawTargetNote">可选。对C2S这种,谱面中原始记录了targetNote的类型的格式,可以将相关记录通过这个字典传过来,供本函数作为选择previous时的优先和参考。</param>
23+
*/
24+
protected virtual void FillAllPrevious(ChuChart chart, List<Alert> alerts, Dictionary<ChuNote, string>? rawTargetNote = null)
25+
{
26+
if (chart.Notes.Count == 0) return;
27+
28+
var endDict = new Dictionary<(Rationals.Rational EndTime, int EndCell, int EndWidth), List<ChuNote>>();
29+
foreach (var n in chart.Notes)
30+
{
31+
endDict.Add((n.EndTime, n.EndCell, n.EndWidth), n);
32+
}
33+
34+
foreach (var cur in chart.Notes)
35+
{
36+
if (!NeedsPrevious(cur)) continue;
37+
if (cur.Previous != null) continue; // 若某些 parser 已提前填了 Previous,则保留
38+
39+
var key = (cur.Time, cur.Cell, cur.Width);
40+
var filtered = FilterPreviousCandidates(cur, endDict.GetValueOrDefault(key, []));
41+
42+
if (rawTargetNote != null && rawTargetNote.TryGetValue(cur, out var target) && !string.IsNullOrEmpty(target))
43+
{
44+
var filteredByRaw = filtered.Where(x=>x.Type == target).ToList();
45+
if (filteredByRaw.Count == 0)
46+
{
47+
alerts.Add(new Alert(Alert.LEVEL.Warning, "未找到声明的前驱/依附音符", cur.Time, (double)chart.ToSecond(cur.Time)));
48+
}
49+
else filtered = filteredByRaw; // 缩小目标范围
50+
}
51+
52+
if (filtered.Count > 0) cur.Previous = filtered[0]; // 取第一个
53+
}
54+
}
55+
56+
private static bool NeedsPrevious(ChuNote n)
57+
{
58+
return IsSlide(n.Type) || IsAir(n.Type) || IsAirSlide(n.Type) || IsAirHold(n.Type) || IsAirCrush(n.Type);
59+
}
60+
61+
public static bool IsSlide(string t) => t is "SLD" or "SLC" or "SXD" or "SXC";
62+
public static bool IsAirSlide(string t) => t is "ASD" or "ASC";
63+
public static bool IsAir(string t) => t is "AIR" or "AUR" or "AUL" or "ADW" or "ADR" or "ADL";
64+
public static bool IsAirHold(string t) => t is "AHD" or "AHX";
65+
public static bool IsAirCrush(string t) => t is "ALD";
66+
// 是否是广义的air音符(Air/Air Hold/Air Slide/Air Crush)
67+
public static bool IsGeneralizedAir(string t) => IsAir(t) || IsAirHold(t) || IsAirSlide(t) || IsAirCrush(t);
68+
69+
private static List<ChuNote> FilterPreviousCandidates(ChuNote cur, List<ChuNote> candidates)
70+
{ // 注意:候选列表已满足“首尾相接”,这里仅做类型约束
71+
List<ChuNote> result = [];
72+
73+
if (IsSlide(cur.Type))
74+
{ // Slide 的 previous:上一段 slide(找不到则说明是首段,则为 null)
75+
result.AddRange(candidates.Where(n => IsSlide(n.Type)));
76+
}
77+
else if (IsAirSlide(cur.Type))
78+
{ // Air Slide:优先匹配“上一段airslide”,其次匹配“上一段其他
79+
result.AddRange(candidates.Where(n => IsAirSlide(n.Type)));
80+
result.AddRange(candidates.Where(n => IsLegalPreviousForAir(n.Type)));
81+
}
82+
else if (IsAir(cur.Type) || IsAirHold(cur.Type))
83+
{ // Air 系列:依附在一个“非广义Air”的音符上
84+
return candidates.Where(n => IsLegalPreviousForAir(n.Type)).ToList();
85+
}
86+
return result;
87+
88+
bool IsLegalPreviousForAir(string t) => !(IsGeneralizedAir(t) || t == "MNE" || t == "CLICK");
89+
}
90+
}

parser/chu/C2sParser.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,18 @@ namespace MuConvert.chu;
1111
* C2S 格式解析器(官方格式,RESOLUTION=384 tick/小节)。
1212
* Tab 分隔文本,识别 HEADER / TIMING / NOTES 区段。
1313
*/
14-
public class C2sParser : IParser<ChuChart>
14+
public class C2sParser: BaseChuParser
1515
{
1616
private static int RSL = 384;
1717
private static readonly HashSet<string> HeadTags = new(StringComparer.OrdinalIgnoreCase)
1818
{ "VERSION", "MUSIC", "SEQUENCEID", "DIFFICULT", "LEVEL", "CREATOR", "BPM_DEF", "MET_DEF", "RESOLUTION", "CLK_DEF", "PROGJUDGE_BPM", "PROGJUDGE_AER", "TUTORIAL" };
1919
private static readonly HashSet<string> TimingTags = new(StringComparer.OrdinalIgnoreCase)
2020
{ "BPM", "MET", "SFL" };
2121

22-
public (ChuChart, List<Alert>) Parse(string text)
22+
// C2S 会原始记录 targetNote 字符串;用于在 Previous 推断有多个候选时优先匹配。
23+
private readonly Dictionary<ChuNote, string> _rawTargetNote = new();
24+
25+
public override (ChuChart, List<Alert>) Parse(string text)
2326
{
2427
var chart = new ChuChart();
2528
var alerts = new List<Alert>();
@@ -51,6 +54,7 @@ public class C2sParser : IParser<ChuChart>
5154
}
5255
}
5356

57+
FillAllPrevious(chart, alerts, _rawTargetNote);
5458
return (chart, alerts);
5559
}
5660

@@ -86,10 +90,11 @@ private static void ParseTiming(string[] p, ChuChart chart)
8690
}
8791
}
8892

89-
private static void ParseNote(string[] p, ChuChart chart, List<Alert> alerts, int lineNum)
93+
private void ParseNote(string[] p, ChuChart chart, List<Alert> alerts, int lineNum)
9094
{
9195
var tag = p[0].ToUpperInvariant();
9296
var note = new ChuNote { Type = tag, Time = Int(p, 1) + new Rational(Int(p, 2), RSL) };
97+
string? targetNote = null;
9398

9499
switch (tag)
95100
{
@@ -107,12 +112,12 @@ private static void ParseNote(string[] p, ChuChart chart, List<Alert> alerts, in
107112
case "FLK":
108113
note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); note.Tag = Str(p, 5); break;
109114
case "AIR": case "AUR": case "AUL": case "ADW": case "ADR": case "ADL":
110-
note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); note.TargetNote = Str(p, 5);
115+
note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1)); targetNote = Str(p, 5);
111116
if (p.Length >= 7) note.Tag = Str(p, 6);
112117
break;
113118
case "AHD": case "AHX":
114119
note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1));
115-
note.TargetNote = Str(p, 5); note.Duration = new Rational(Int(p, 6), RSL);
120+
targetNote = Str(p, 5); note.Duration = new Rational(Int(p, 6), RSL);
116121
if (p.Length >= 8) note.Tag = Str(p, 7);
117122
break;
118123
case "ASD": case "ASC":
@@ -123,7 +128,7 @@ private static void ParseNote(string[] p, ChuChart chart, List<Alert> alerts, in
123128
return;
124129
}
125130
note.Cell = Int(p, 3); note.Width = Math.Max(1, Int(p, 4, 1));
126-
note.TargetNote = Str(p, 5);
131+
targetNote = Str(p, 5);
127132
note.ExtraData = [Int(p, 6), Int(p, 10)];
128133
note.Duration = new Rational(Int(p, 7), RSL);
129134
note.EndCell = Int(p, 8); note.EndWidth = Math.Max(1, Int(p, 9, 1));
@@ -144,6 +149,7 @@ private static void ParseNote(string[] p, ChuChart chart, List<Alert> alerts, in
144149
alerts.Add(new Alert(Warning, string.Format(Locale.C2SUnknownNoteType, tag)) { Line = lineNum }); return;
145150
}
146151

152+
if (targetNote != null) _rawTargetNote[note] = targetNote;
147153
chart.Notes.Add(note);
148154
}
149155

parser/chu/SusParser.cs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace MuConvert.chu;
1111
* SUS 格式解析器(社区工具格式,REQUEST=480 tick/拍,lane 0–31)。
1212
* #MMTT:data 十六进制编码音符。
1313
*/
14-
public class SusParser : IParser<ChuChart>
14+
public class SusParser: BaseChuParser
1515
{
1616
private static int RSL = 480 * 4;
1717

@@ -28,7 +28,7 @@ public class SusParser : IParser<ChuChart>
2828
[0x10] = "MNE",
2929
};
3030

31-
public (ChuChart, List<Alert>) Parse(string text)
31+
public override (ChuChart, List<Alert>) Parse(string text)
3232
{
3333
var chart = new ChuChart();
3434
var alerts = new List<Alert>();
@@ -57,6 +57,7 @@ public class SusParser : IParser<ChuChart>
5757
}
5858
}
5959

60+
FillAllPrevious(chart, alerts);
6061
return (chart, alerts);
6162
}
6263

@@ -208,11 +209,7 @@ private static void ParseSlideData(string dataStr, ChuNote note, int tpm, List<A
208209

209210
private static void ParseAirTarget(string dataStr, ChuNote note, int tpm, List<Alert> alerts, int lineNum)
210211
{
211-
if (dataStr.Length >= 8)
212-
{
213-
note.TargetNote = HexToInt(dataStr[6..8]).ToString();
214-
}
215-
else
212+
if (dataStr.Length < 8)
216213
{
217214
alerts.Add(new Alert(Warning, $"AIR/ADW 音符缺少目标: {dataStr}") { Line = lineNum, RelevantNote = FormatNoteRef(note, tpm) });
218215
}

parser/chu/UgcParser.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace MuConvert.chu;
1111
* UGC 格式解析器(UMIGURI 格式,@TICKS=480 tick/拍)。
1212
* @HEADER 标签 + #measure'tick:code 音符格式。
1313
*/
14-
public class UgcParser : IParser<ChuChart>
14+
public class UgcParser: BaseChuParser
1515
{
1616
private static int RSL = 480 * 4;
1717

@@ -33,7 +33,7 @@ public class UgcParser : IParser<ChuChart>
3333
["C"] = "CE",
3434
};
3535

36-
public (ChuChart, List<Alert>) Parse(string text)
36+
public override (ChuChart, List<Alert>) Parse(string text)
3737
{
3838
var chart = new ChuChart();
3939
var alerts = new List<Alert>();
@@ -64,6 +64,7 @@ public class UgcParser : IParser<ChuChart>
6464
}
6565

6666
FinalizeUgcSflDurations(chart);
67+
FillAllPrevious(chart, alerts);
6768
return (chart, alerts);
6869
}
6970

@@ -383,7 +384,7 @@ private static int ParseSlideNote(string[] lines, int idx, string code, ChuNote
383384
Time = previousNote.EndTime, Cell = previousNote.EndCell, Width = previousNote.EndWidth,
384385
Duration = segmentEnd - previousNote.EndTime,
385386
EndCell = endCell, EndWidth = endWidth,
386-
TargetNote = "SLD"
387+
Previous = foundFirst ? previousNote : null,
387388
};
388389
chart.Notes.Add(note);
389390
previousNote = note;
@@ -475,8 +476,6 @@ private static void ParseAirNote(string code, ChuNote note, List<Alert> alerts,
475476
alerts.Add(new Alert(Warning, $"未知的 AIR 方向: {dir}") { Line = lineNum, RelevantNote = FormatNoteRef(note, chart) });
476477
}
477478

478-
note.TargetNote = mainPart.Length > 2 ? mainPart[2..] : "N";
479-
480479
if (underscoreIdx >= 0 && note.Type == "AHD")
481480
{
482481
var durStr = afterCellWidth[(underscoreIdx + 1)..];

tests/chu/ChuTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ private static ChuNote C2sNoteScaledToUgcTicks(ChuNote n, int ugcTicksPerBeat, i
102102
Duration = duration,
103103
EndCell = n.EndCell,
104104
EndWidth = n.EndWidth,
105-
TargetNote = n.TargetNote,
105+
Previous = n.Previous,
106106
Tag = n.Tag,
107107
ExtraData = [..n.ExtraData],
108108
};
@@ -115,6 +115,7 @@ private static void AssertUgcNotesEquivalentToReparsedC2s(ChuChart ugc, ChuChart
115115
if (isUgcReference)
116116
{
117117
var ugcSnaps = ugc.Notes
118+
.Where(n=>n.Type != "CLICK")
118119
.Select(n => SnapshotNote(UgcNoteScaledToC2sTicks(n, 480, 384)))
119120
.OrderBy(s => s)
120121
.ToArray();

0 commit comments

Comments
 (0)