Skip to content

Commit b11c121

Browse files
committed
[+] Simai片段测试
1 parent f92777d commit b11c121

10 files changed

Lines changed: 251 additions & 62 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ obj/
44
riderModule.iml
55
/_ReSharper.Caches/
66
/.idea/
7-
*.DotSettings.user
7+
*.DotSettings.user
8+
.cursor

generator/MA2Generator.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ public override string ToString()
1717
}
1818
};
1919

20+
#pragma warning disable CS8618
2021
public MA2Generator(bool isUtage = false)
22+
#pragma warning restore CS8618
2123
{
2224
IsUtage = isUtage;
2325
}

tests/MA2_103测试.cs

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -55,44 +55,6 @@ public void Simai转MA2_103(TestInput input)
5555
AssertMa2NotesEqual(expectedMa2, ma2, input.ToString());
5656
}
5757

58-
/// <summary>
59-
/// 提取音符段至 <c>T_REC</c> 之前:跳过头部与 <c>BPM</c> 行;若存在 <c>MET\t</c> 小节行则跳过该行;
60-
/// 部分旧官谱 golden 无 <c>MET</c>,则在 <c>BPM</c> 块后的首条非头行开始收集。
61-
/// </summary>
62-
private static string KeepNotesOnly(string text)
63-
{
64-
var result = new StringBuilder();
65-
var inNotes = false;
66-
foreach (var l in text.EnumerateLines())
67-
{
68-
var line = l.ToString().TrimEnd('\r');
69-
if (string.IsNullOrWhiteSpace(line)) continue;
70-
if (line.StartsWith("T_REC", StringComparison.Ordinal)) break;
71-
72-
if (!inNotes)
73-
{
74-
if (IsMa2HeaderOrBpmLine(line)) continue;
75-
if (line.StartsWith("MET\t", StringComparison.Ordinal)) continue;
76-
inNotes = true;
77-
}
78-
79-
result.Append(line).Append('\n');
80-
}
81-
82-
return result.ToString();
83-
}
84-
85-
private static bool IsMa2HeaderOrBpmLine(string line) =>
86-
line.StartsWith("VERSION\t", StringComparison.Ordinal) ||
87-
line.StartsWith("FES_MODE\t", StringComparison.Ordinal) ||
88-
line.StartsWith("BPM_DEF\t", StringComparison.Ordinal) ||
89-
line.StartsWith("MET_DEF\t", StringComparison.Ordinal) ||
90-
line.StartsWith("RESOLUTION\t", StringComparison.Ordinal) ||
91-
line.StartsWith("CLK_DEF\t", StringComparison.Ordinal) ||
92-
line.StartsWith("COMPATIBLE_CODE\t", StringComparison.Ordinal) ||
93-
line.StartsWith("GENERATED_BY\t", StringComparison.Ordinal) ||
94-
line.StartsWith("BPM\t", StringComparison.Ordinal);
95-
9658
private static (int TimeTick, int Len, string Extra) GetSlideTime(string slide)
9759
{
9860
var values = slide.Split('\t');

tests/MuConvert.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
1313
<PackageReference Include="xunit" Version="2.9.3" />
1414
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
15+
<PackageReference Include="YamlDotNet" Version="17.0.1" />
1516
</ItemGroup>
1617

1718
<ItemGroup>

tests/Simai片段测试.cs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using System.Globalization;
2+
using System.Text;
3+
using MuConvert.generator;
4+
using MuConvert.parser;
5+
using MuConvert.utils;
6+
using static MuConvert.Tests.TestUtils;
7+
8+
namespace MuConvert.Tests;
9+
10+
public class Simai片段测试
11+
{
12+
public static IEnumerable<object[]> FragmentYamlFiles()
13+
{
14+
var root = Path.Combine(FindRepoRoot().FullName, "tests", "testset", "片段");
15+
if (!Directory.Exists(root))
16+
throw new DirectoryNotFoundException($"片段测例目录不存在: {root}");
17+
18+
foreach (var path in Directory.EnumerateFiles(root, "*.yaml", SearchOption.TopDirectoryOnly)
19+
.OrderBy(p => p, StringComparer.Ordinal))
20+
yield return [TestSegment.Load(path)];
21+
}
22+
23+
[Theory]
24+
[MemberData(nameof(FragmentYamlFiles))]
25+
public void Simai片段转MA2(TestSegment c)
26+
{
27+
var (chart, parseAlerts) = new SimaiParser().Parse(c.Simai);
28+
var (ma2Full, genAlerts) = new MA2Generator(isUtage: false).Generate(chart);
29+
30+
var actual = KeepNotesOnly(ma2Full);
31+
var expected = NormalizeMa2Block(c.Ma2);
32+
AssertMa2NotesEqual(expected, actual, c.ToString());
33+
}
34+
35+
private static string NormalizeMa2Block(string text)
36+
{
37+
// 与生成结果一致:统一换行、去掉文末空行
38+
var sb = new StringBuilder();
39+
foreach (var l in text.EnumerateLines())
40+
{
41+
var line = l.ToString().TrimEnd('\r');
42+
if (string.IsNullOrWhiteSpace(line) && sb.Length == 0)
43+
continue;
44+
sb.Append(line).Append('\n');
45+
}
46+
while (sb.Length > 0 && sb[^1] == '\n' && (sb.Length == 1 || sb[^2] == '\n'))
47+
sb.Length--;
48+
return sb.ToString().TrimEnd();
49+
}
50+
51+
private static (int TimeTick, int Len, string Extra) GetSlideTime(string slide)
52+
{
53+
var values = slide.Split('\t');
54+
return (int.Parse(values[1], CultureInfo.InvariantCulture) * 384 + int.Parse(values[2], CultureInfo.InvariantCulture),
55+
int.Parse(values[5], CultureInfo.InvariantCulture),
56+
string.Join("\t", values[0], values[3], values[4], values[6]));
57+
}
58+
59+
private static bool CompareLine(string exp, string act)
60+
{
61+
var result = string.Equals(exp, act, StringComparison.Ordinal);
62+
if (!result && exp.Length >= 5 && act.Length >= 5 && exp[..5] == act[..5] && SlideTypeTool.IsSlide(exp[2..5]))
63+
{
64+
var (expTime, expLen, expExtra) = GetSlideTime(exp);
65+
var (actTime, actLen, actExtra) = GetSlideTime(act);
66+
if (expExtra != actExtra) return result;
67+
if (exp[..2] == "CN")
68+
{
69+
if (expTime + expLen == actTime + actLen || Math.Abs(expLen - actLen) <= 1) result = true;
70+
}
71+
else
72+
{
73+
if (expTime == actTime && Math.Abs(expLen - actLen) <= 1) result = true;
74+
}
75+
}
76+
77+
return result;
78+
}
79+
80+
private static void AssertMa2NotesEqual(string expected, string actual, string context)
81+
{
82+
var expectedLines = expected.Split('\n');
83+
var actualLines = actual.Split('\n');
84+
var max = Math.Max(expectedLines.Length, actualLines.Length);
85+
86+
for (var i = 0; i < max; i++)
87+
{
88+
var exp = i < expectedLines.Length ? expectedLines[i] : "<EOF>";
89+
var act = i < actualLines.Length ? actualLines[i] : "<EOF>";
90+
var result = CompareLine(exp, act);
91+
if (!result)
92+
{
93+
for (var j = 1; j < Math.Min(expectedLines.Length, i + 5); j++)
94+
{
95+
if (CompareLine(expectedLines[j], act))
96+
{
97+
(expectedLines[j], expectedLines[i]) = (expectedLines[i], expectedLines[j]);
98+
result = true;
99+
break;
100+
}
101+
}
102+
}
103+
104+
if (!result)
105+
{
106+
Assert.Fail(
107+
$"{context}: first difference at line {i + 1}:{Environment.NewLine}" +
108+
$"EXPECTED: {exp}{Environment.NewLine}" +
109+
$"ACTUAL : {act}");
110+
}
111+
}
112+
}
113+
}

tests/Simai转MA2测试.cs

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using MuConvert.parser;
55
using MuConvert.utils;
66
using Xunit.Abstractions;
7+
using static MuConvert.Tests.TestUtils;
78

89
namespace MuConvert.Tests;
910

@@ -31,31 +32,11 @@ private void TestChart(TestInput input)
3132
var (ma2, _) = new MA2Generator(isUtage: false).Generate(chart);
3233

3334
Assert.Equal(maidata.ClockCount * 96, TestUtils.TryParseMa2ClkDef(ma2));
34-
ma2 = keepNotesOnly(ma2);
35-
expectedMa2 = keepNotesOnly(expectedMa2);
35+
ma2 = KeepNotesOnly(ma2);
36+
expectedMa2 = KeepNotesOnly(expectedMa2);
3637
AssertTextEqual(expectedMa2, ma2);
3738
}
3839

39-
private static string keepNotesOnly(string text)
40-
{
41-
var result = new StringBuilder();
42-
var METEncountered = false;
43-
foreach (var l in text.EnumerateLines())
44-
{
45-
var line = l.ToString();
46-
if (string.IsNullOrWhiteSpace(line)) continue;
47-
if (line.StartsWith("MET\t"))
48-
{
49-
METEncountered = true;
50-
continue;
51-
}
52-
if (!METEncountered) continue;
53-
if (line.StartsWith("T_REC")) break; // 结束
54-
result.Append(line + "\n");
55-
}
56-
return result.ToString();
57-
}
58-
5940
private static (int, int, string) GetSlideTime(string slide)
6041
{
6142
var values = slide.Split("\t");

tests/TestUtils.cs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using MuConvert.maidata;
55
using MuConvert.parser;
66
using MuConvert.utils;
7+
using YamlDotNet.Serialization;
78

89
namespace MuConvert.Tests;
910

@@ -52,6 +53,44 @@ public static DirectoryInfo FindRepoRoot()
5253
}
5354
return null;
5455
}
56+
57+
/// <summary>
58+
/// 提取 MA2 音符段至 <c>T_REC</c> 之前:跳过头部与 <c>BPM</c> 行;若存在 <c>MET\t</c> 小节行则跳过该行;
59+
/// 部分旧官谱 golden 无 <c>MET</c>,则在 <c>BPM</c> 块后的首条非头行开始收集。与 <see cref="Simai片段测试"/> / <see cref="MA2_103测试"/> 断言用逻辑一致。
60+
/// </summary>
61+
public static string KeepNotesOnly(string text)
62+
{
63+
var result = new StringBuilder();
64+
var inNotes = false;
65+
foreach (var l in text.EnumerateLines())
66+
{
67+
var line = l.ToString().TrimEnd('\r');
68+
if (string.IsNullOrWhiteSpace(line)) continue;
69+
if (line.StartsWith("T_REC", StringComparison.Ordinal)) break;
70+
71+
if (!inNotes)
72+
{
73+
if (IsMa2HeaderOrBpmLine(line)) continue;
74+
if (line.StartsWith("MET\t", StringComparison.Ordinal)) continue;
75+
inNotes = true;
76+
}
77+
78+
result.Append(line).Append('\n');
79+
}
80+
81+
return result.ToString().Trim();
82+
}
83+
84+
private static bool IsMa2HeaderOrBpmLine(string line) =>
85+
line.StartsWith("VERSION\t", StringComparison.Ordinal) ||
86+
line.StartsWith("FES_MODE\t", StringComparison.Ordinal) ||
87+
line.StartsWith("BPM_DEF\t", StringComparison.Ordinal) ||
88+
line.StartsWith("MET_DEF\t", StringComparison.Ordinal) ||
89+
line.StartsWith("RESOLUTION\t", StringComparison.Ordinal) ||
90+
line.StartsWith("CLK_DEF\t", StringComparison.Ordinal) ||
91+
line.StartsWith("COMPATIBLE_CODE\t", StringComparison.Ordinal) ||
92+
line.StartsWith("GENERATED_BY\t", StringComparison.Ordinal) ||
93+
line.StartsWith("BPM\t", StringComparison.Ordinal);
5594

5695
public static Chart LoadOneChart(out List<Alert> alerts)
5796
{
@@ -96,6 +135,42 @@ public static IEnumerable<object[]> GetTestInputs(string dataDir, int? lv = null
96135
}
97136
}
98137

138+
public sealed class TestSegment
139+
{
140+
private static readonly IDeserializer YamlDeserializer = new DeserializerBuilder()
141+
.IgnoreUnmatchedProperties()
142+
.Build();
143+
144+
/// <summary>YAML 文件的原始文件名(不含目录)。</summary>
145+
[YamlIgnore]
146+
public string YamlFileName { get; private set; } = "";
147+
[YamlMember(Alias = "simai")]
148+
public string Simai { get; set; } = "";
149+
[YamlMember(Alias = "ma2")]
150+
public string Ma2 { get; set; } = "";
151+
152+
public override string ToString() => YamlFileName;
153+
154+
public static TestSegment Load(string yamlPath)
155+
{
156+
var yamlFileName = Path.GetFileName(yamlPath);
157+
var text = File.ReadAllText(yamlPath, Encoding.UTF8);
158+
var seg = YamlDeserializer.Deserialize<TestSegment>(text)
159+
?? throw new FormatException($"{yamlPath}: 空 YAML 或根节点无法解析为映射");
160+
161+
seg.YamlFileName = yamlFileName;
162+
seg.Simai = seg.Simai.Trim();
163+
seg.Ma2 = seg.Ma2.Trim();
164+
165+
if (string.IsNullOrWhiteSpace(seg.Simai))
166+
throw new FormatException($"{yamlPath}: 缺少或为空 simai");
167+
if (string.IsNullOrWhiteSpace(seg.Ma2))
168+
throw new FormatException($"{yamlPath}: 缺少或为空 ma2");
169+
170+
return seg;
171+
}
172+
}
173+
99174
public record TestInput(string Maidata, int LevelId)
100175
{
101176
public string Dir = Path.GetDirectoryName(Maidata)!;

tests/testset/官谱/Believe the Rainbow/02.ma2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ FES_MODE 0
33
BPM_DEF 133.000 133.000 133.000 133.000
44
MET_DEF 4 4
55
RESOLUTION 384
6-
CLK_DEF 380
6+
CLK_DEF 384
77
COMPATIBLE_CODE MA2
88

99
BPM 0 0 133.000
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
simai: "{8}2b,,74,,26,26,5/3-8[8:1],64,7b/A3f,,,,"
2+
ma2: |
3+
BRTAP 0 0 1
4+
NMTAP 0 96 6
5+
NMTAP 0 96 3
6+
NMTAP 0 192 1
7+
NMTAP 0 192 5
8+
NMTAP 0 240 1
9+
NMTAP 0 240 5
10+
NMTAP 0 288 4
11+
NMSTR 0 288 2
12+
NMSI_ 0 288 2 96 48 7
13+
NMTAP 0 336 5
14+
NMTAP 0 336 3
15+
NMTTP 1 0 2 A 1 M1
16+
BRTAP 1 0 6
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
simai: "{8}6q2q6q2q6q2q6[8:18]b*q2q6q2q6q2[8:15]*q2q6q2q6[8:12]*q2q6q2[8:9]b*q2q6[8:6]b*q2[8:3],,8,8x,7x,6x,5x,,E8,,5,5,,,E8/A8,,5,5,,,E8f/A8,"
2+
ma2: |
3+
NMSTR 0 0 5
4+
BRSUR 0 0 5 96 144 1
5+
NMSUR 0 0 5 96 144 1
6+
NMSUR 0 0 5 96 144 1
7+
BRSUR 0 0 5 96 144 1
8+
BRSUR 0 0 5 96 144 1
9+
NMSUR 0 0 5 96 144 1
10+
NMTAP 0 96 7
11+
EXTAP 0 144 7
12+
EXTAP 0 192 6
13+
EXTAP 0 240 5
14+
CNSUR 0 240 1 0 144 5
15+
CNSUR 0 240 1 0 144 5
16+
CNSUR 0 240 1 0 144 5
17+
CNSUR 0 240 1 0 144 5
18+
CNSUR 0 240 1 0 144 5
19+
EXTAP 0 288 4
20+
NMTTP 1 0 7 E 0 M1
21+
CNSUR 1 0 5 0 144 1
22+
CNSUR 1 0 5 0 144 1
23+
CNSUR 1 0 5 0 144 1
24+
CNSUR 1 0 5 0 144 1
25+
NMTAP 1 96 4
26+
NMTAP 1 144 4
27+
CNSUR 1 144 1 0 144 5
28+
CNSUR 1 144 1 0 144 5
29+
CNSUR 1 144 1 0 144 5
30+
NMTTP 1 288 7 E 0 M1
31+
NMTTP 1 288 7 A 0 M1
32+
CNSUR 1 288 5 0 144 1
33+
CNSUR 1 288 5 0 144 1
34+
NMTAP 2 0 4
35+
NMTAP 2 48 4
36+
CNSUR 2 48 1 0 144 5
37+
NMTTP 2 192 7 E 1 M1
38+
NMTTP 2 192 7 A 0 M1

0 commit comments

Comments
 (0)