Skip to content

Commit 14a985c

Browse files
committed
Merge branch 'master' into dev
2 parents 94019c6 + fa5e32f commit 14a985c

7 files changed

Lines changed: 96 additions & 37 deletions

File tree

chart/BaseChart.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@
33

44
namespace MuConvert.chart;
55

6-
public interface IBaseChart;
6+
// 这个接口其实没什么实际作用,因为绝大多数情况下用的都是具体的Chart或下面的带泛型的BaseChart。
7+
// 只有极个别的情况:不依赖Notes等BaseChart内部的属性,只是希望有个类名表示Chart;而且不希望用泛型(写起来太复杂/构造函数无法使用泛型等情况),才会改为使用IBaseChart。
8+
// 这个接口应尽量保持简洁、尽量不声明东西,声明都放在抽象类BaseChart里面去。
9+
public interface IBaseChart
10+
{
11+
// 如上所说这个接口应尽量保持简洁。但为什么要在这放这样一个声明呢,因为Alert的二号构造函数内需要用到ToSecond方法,而构造函数是不允许泛型的、只能使用IBaseChart。所以只好把ToSecond的声明加到接口里。
12+
public Rational ToSecond(Rational barTime);
13+
}
714

815
/**
916
* 所有的谱面均应该继承自此类。

generator/chu/UgcGenerator.cs

Lines changed: 32 additions & 3 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,则应当已经被处理过了,直接跳过
@@ -68,7 +97,7 @@ private string Serialize(ChuChart ugc, List<Alert> alerts)
6897
var ucode = UCode(n);
6998
if (ucode == "")
7099
{
71-
alerts.Add(new Alert(Alert.LEVEL.Warning, $"UGC Generator遇到了不支持的音符类型: {n.Type}", n.Time, (double)ugc.ToSecond(n.Time)));
100+
alerts.Add(new Alert(Alert.LEVEL.Warning, $"UGC Generator遇到了不支持的音符类型: {n.Type}", (ugc, n.Time)));
72101
continue;
73102
}
74103
sb.Append($"#{m}'{o}:{ucode}");

generator/mai/MA2Generator.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,11 @@ protected void GenerateStatistics(StringBuilder result, Statistics stats)
302302
{
303303
if (chart != null) throw new Exception(Locale.InstanceMultipleUsage);
304304
chart = _chart;
305+
if (chart.Notes.Count == 0)
306+
{
307+
alerts.Add(new Alert(Error, Locale.NoNotesInChart));
308+
throw new ConversionException(alerts);
309+
}
305310
chart.Sort();
306311
StringBuilder result = new StringBuilder();
307312

parser/chu/BaseChuParser.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ protected virtual void FillAllPrevious(ChuChart chart, List<Alert> alerts, Dicti
4646
var filteredByRaw = filtered.Where(x=>x.Type == target).ToList();
4747
if (filteredByRaw.Count == 0)
4848
{
49-
alerts.Add(new Alert(Alert.LEVEL.Warning, "未找到声明的前驱/依附音符", cur.Time, (double)chart.ToSecond(cur.Time)));
49+
alerts.Add(new Alert(Alert.LEVEL.Warning, "未找到声明的前驱/依附音符", (chart, cur.Time)));
5050
}
5151
else filtered = filteredByRaw; // 缩小目标范围
5252
}
@@ -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: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public override (ChuChart, List<Alert>) Parse(string text)
3434

3535
if (inHeader)
3636
{
37-
if (line == "@ENDHEAD")
37+
if (line.StartsWith("@ENDHEAD"))
3838
{
3939
inHeader = false;
4040
continue;
@@ -183,7 +183,9 @@ private void ParseHeaderLine(string line, ChuChart chart, List<Alert> alerts, in
183183
case "@EXVER": case "@SORT": case "@BGM": case "@BGMOFS": case "@BGMPRV":
184184
case "@JACKET": case "@BGIMG": case "@BGMODE": case "@FLDCOL": case "@FLDIMG":
185185
case "@FLAG": case "@ATINFO": case "@DLURL": case "@COPYRIGHT": case "@LICENSE":
186-
case "@MAINTIL": case "@TIL":
186+
case "@MAINTIL": case "@TIL": case "@USETIL":
187+
case "@MAINBPM":
188+
case "@BGSCENE": case "@FLDSCENE": case "@RLDATE": case "@CMT":
187189
break;
188190

189191
case "@SPDMOD":
@@ -313,7 +315,7 @@ private int ParseNoteLine(string[] lines, int idx, ChuChart chart, List<Alert> a
313315
break;
314316

315317
default:
316-
alerts.Add(new Alert(Warning, $"未知的音符类型前缀 '{typeChar}': {line}", note.Time, (double)chart.ToSecond(note.Time), lineNum, line));
318+
alerts.Add(new Alert(Warning, $"未知的音符类型前缀 '{typeChar}': {line}", (chart, note.Time), lineNum, line));
317319
// 如果后面跟的是跟随行(子ノーツ)而非主行(親ノーツ)的话,把它们全部消耗掉
318320
while (idx + 1 < lines.Length)
319321
{
@@ -406,6 +408,23 @@ private int ParseHoldNote(bool isAirHold, string[] lines, int idx, string code,
406408
alerts.Add(new Alert(Warning, $"HLD 音符缺少时长跟随行") { Line = idx + 1, RelevantNote = lines[idx] });
407409
return idx;
408410
}
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+
}
409428

410429
private int ParseSlideNote(bool isAirSlide, string[] lines, int idx, string code, ChuNote previousNote, List<Alert> alerts, ChuChart chart)
411430
{
@@ -439,6 +458,12 @@ private int ParseSlideNote(bool isAirSlide, string[] lines, int idx, string code
439458
EndHeight = endHeight != null ? U2C_Height(endHeight.Value) : previousNote.EndHeight,
440459
Previous = foundFirst ? previousNote : null,
441460
};
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+
}
442467

443468
chart.Notes.Add(note);
444469
previousNote = note;
@@ -496,51 +521,42 @@ private void ParseCellWidth(string code, int startIdx, ChuNote note, List<Alert>
496521
if (code.Length > startIdx + 1)
497522
note.Width = HToI(code[startIdx + 1]);
498523
else
499-
alerts.Add(new Alert(Warning, $"音符缺少 width: {code}", note.Time, (double)chart.ToSecond(note.Time), lineNum, FormatNoteRef(note, code)));
524+
alerts.Add(new Alert(Warning, $"音符缺少 width: {code}", (chart, note.Time), lineNum, FormatNoteRef(note, code)));
500525
}
501526
else
502527
{
503-
alerts.Add(new Alert(Warning, $"音符缺少 cell 和 width: {code}", note.Time, (double)chart.ToSecond(note.Time), lineNum, FormatNoteRef(note, code)));
528+
alerts.Add(new Alert(Warning, $"音符缺少 cell 和 width: {code}", (chart, note.Time), lineNum, FormatNoteRef(note, code)));
504529
}
505530
}
506531

507532
private void ParseAirNote(string code, ChuNote note, List<Alert> alerts, int lineNum, ChuChart chart)
508533
{
534+
note.Type = "AIR"; // 出错情况下的缺省值
509535
if (code.Length < 5)
510536
{
511537
alerts.Add(new Alert(Warning, $"AIR 音符代码过短: {code}") { Line = lineNum });
512-
note.Type = "AIR";
513538
return;
514539
}
515540

516541
ParseCellWidth(code, 1, note, alerts, lineNum, chart);
517542
var mainPart = code[3..];
518543

519-
if (mainPart.Length < 2)
520-
{
521-
alerts.Add(new Alert(Warning, $"AIR 音符方向代码过短: {code}") { Line = lineNum });
522-
note.Type = "AIR";
523-
return;
524-
}
525-
526-
var dir = mainPart[..2];
527-
if (U2C_AirDirections.TryGetValue(dir, out var airType))
528-
{
529-
note.Type = airType;
530-
}
531-
else
532-
{
533-
note.Type = "AIR";
534-
alerts.Add(new Alert(Warning, $"未知的 AIR 方向: {dir}") { Line = lineNum, RelevantNote = FormatNoteRef(note, code) });
535-
}
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+
// 解析颜色
536549
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));
537553
}
538554

539555
private int ParseAirCrushNote(string[] lines, int idx, string code, ChuNote note, List<Alert> alerts, ChuChart chart)
540556
{
541557
note.Type = "ALD";
542558
ParseCellWidth(code, 1, note, alerts, idx + 1, chart);
543-
if (code.Length <= 3) alerts.Add(new Alert(Warning, "AirCrush缺少参数!", note.Time, (double)chart.ToSecond(note.Time), idx+1, lines[idx]));
559+
if (code.Length <= 3) alerts.Add(new Alert(Warning, "AirCrush缺少参数!", (chart, note.Time), idx+1, lines[idx]));
544560
else ParseHeightAndColor(note, code[3..], alerts, idx+1, "C");
545561

546562
bool foundFirst = false;
@@ -555,7 +571,7 @@ private int ParseAirCrushNote(string[] lines, int idx, string code, ChuNote note
555571
}
556572

557573
if (Version >= 8 && marker != "c")
558-
alerts.Add(new Alert(Warning, $"Air-Crush(v8)子行标记应为 'c',实际为 '{marker}'", note.Time, (double)chart.ToSecond(note.Time), idx + 1, nextLine));
574+
alerts.Add(new Alert(Warning, $"Air-Crush(v8)子行标记应为 'c',实际为 '{marker}'", (chart, note.Time), idx + 1, nextLine));
559575

560576
if (Version <= 6 && !intervalSet && marker == "s")
561577
{

tests/chu/ChuTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ public class ChuTests
1616
public static IEnumerable<object[]> OfficialC2sChartPaths()
1717
{
1818
return Directory.EnumerateFiles(OfficialDir, "*.c2s", SearchOption.AllDirectories)
19-
.OrderBy(p => p, StringComparer.OrdinalIgnoreCase).Select(path => (object[])[path]);
19+
.OrderBy(p => p, StringComparer.OrdinalIgnoreCase).Select(path => (object[])[Path.GetRelativePath(Environment.CurrentDirectory, path)]);
2020
}
2121

2222
public static IEnumerable<object[]> CustomUgcChartPaths()
2323
{
2424
return Directory.EnumerateFiles(CustomDir, "*.ugc", SearchOption.AllDirectories)
25-
.OrderBy(p => p, StringComparer.OrdinalIgnoreCase).Select(path => (object[])[path]);
25+
.OrderBy(p => p, StringComparer.OrdinalIgnoreCase).Select(path => (object[])[Path.GetRelativePath(Environment.CurrentDirectory, path)]);
2626
}
2727

2828
[Theory]

utils/Error.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Rationals;
1+
using MuConvert.chart;
2+
using Rationals;
23

34
namespace MuConvert.utils;
45

@@ -34,12 +35,13 @@ public Alert(LEVEL level, string description, Rational? timeInBar = null, double
3435
TimeInSeconds = timeInSeconds;
3536
}
3637

37-
public Alert(LEVEL level, string description, (mai.MaiChart, Rational) barTime, int? line = null, string? relevantNote = null)
38+
public Alert(LEVEL level, string description, (IBaseChart, Rational) barTime, int? line = null, string? relevantNote = null)
3839
: this(level, description, line, relevantNote)
3940
{
4041
var (chart, time) = barTime;
41-
TimeInBar = time;
42-
if (chart.BpmList.Count > 0) TimeInSeconds = (double)chart.ToSecond(time);
42+
TimeInBar = time;
43+
try { TimeInSeconds = (double)chart.ToSecond(time); }
44+
catch (Exception) { /* 忽略异常。因为异常是由于ToSecond报错引起的,往往是BPMList为空或首项不为0导致的,这种情况直接不显示秒数就好,没必要死磕 */ }
4345
}
4446

4547
public override string ToString()

0 commit comments

Comments
 (0)