Skip to content

Commit 94019c6

Browse files
committed
[D&T]文档,修改增强测试
1 parent 71f0b9a commit 94019c6

3 files changed

Lines changed: 74 additions & 8 deletions

File tree

README.md

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ MuConvert - 新一代多功能音游转谱器
33

44
MuConvert 是一个多功能的音游转谱器。目前支持maimai、chunithm的谱面格式转换,未来还可能加入更多游戏/更多格式支持。
55
- maimai:支持 Simai(自制谱社区最主流格式,[MajdataEdit](https://majdata.net/edit)[Visual Maimai](https://github.com/CH3COOOHH/Visual-Maimai-Release)等都是这种格式)与 MA2(官方游戏格式)的双向互转。
6-
- chunithm:支持 UGC([Umiguri](https://umgr.inonote.jp/en/)的格式)与 C2S(官方游戏格式)的双向互转;
6+
- CHUNITHM:支持 UGC([Umiguri](https://umgr.inonote.jp/en/)的格式)与 C2S(官方游戏格式)的双向互转;
77
- 此外,还实验性地支持SUS与上述格式的双向互转(注意目前支持还不太完善,可能有较多bug,如遇bug欢迎反馈)
8+
- Ongeki:由于这款游戏尚无普遍通用的社区自制谱格式,因此支持的是OGKR(官方游戏格式)的解析和生成逻辑,而非直接的谱面转换。详见下文[Ongeki游戏支持](#Ongeki游戏支持)部分所述。
89

910
> Kind reminder: To reduce developers’ workload, this README is maintained only in Chinese. We recommend using an LLM to translate and read this document.
1011
@@ -15,7 +16,7 @@ MuConvert 是一个多功能的音游转谱器。目前支持maimai、chunithm
1516
- 工具+基础库:既可以直接当作命令行工具使用,也可以把它作为一个C#依赖库嵌入到你的工程里。
1617
- 可扩展的架构设计:本项目以中间表示(Chart类)为核心,通过为每种语言编写parser、将语言解析为统一的中间表示对象,再为每种语言编写generator,实现任意两个语言间的互转。
1718
- 项目设计具有良好的可扩展性,您可轻松按照自己的需求定制自己的语言格式,也可直接把解析得到的Chart对象拿来服务于您自己的下游项目如谱面播放器等。
18-
- 多游戏支持:基于上述良好的可扩展性,本项目一套代码可提供对maimai、chunithm两款游戏共五种格式的支持,未来还可能加入ongeki等更多游戏
19+
- 多游戏支持:基于上述良好的可扩展性,本项目一套代码可提供对maimai、CHUNITHM、Ongeki三款游戏共六种格式的支持,未来还可能加入更多游戏/更多格式
1920

2021
## 使用文档
2122
本项目具有两种使用方式:
@@ -109,6 +110,8 @@ MuConvert "D:\charts\Song\0003_00.c2s"
109110
110111
</details>
111112
113+
> Ongeki游戏支持方面,目前仅支持对OGKR一种格式的解析和生成,不是直接两种格式间转换,因此没有直接调用CLI程序的入口,只能以代码调用的方式使用。详见下文[Ongeki游戏支持](#Ongeki游戏支持)部分所述。
114+
112115
### 2) 将本项目作为依赖库使用
113116
#### 导入依赖库
114117
- **推荐做法**:把本仓库作为 git submodule 引入你的工程仓库,然后把 `MuConvert.csproj` 加入你的 `.sln`/`.slnx`
@@ -160,14 +163,43 @@ return maidataText; // maidataText即为转谱结果
160163
var (c2sChart, alerts) = new C2sParser().Parse(c2sText); // 解析 C2S 谱面字符串
161164
var (ugcChart, alerts) = new UgcParser().Parse(ugcText); // 解析 UGC 谱面字符串
162165
// 以上得到的c2sChart、ugcChart,都是ChuChart类型的谱面表示对象;
163-
// alerts是解析过程中可能产生的警告信息等,建议打印出来。
166+
// alerts是解析过程中可能产生的警告信息等,建议打印出来(直接对Alert对象调用ToString()即可)
164167
165168
var (c2sText, alerts) = new C2sGenerator().Generate(ugcChart); // UGC -> C2S
166169
var (ugcText, alerts) = new UgcGenerator().Generate(c2sChart); // C2S -> UGC
167170
// 各种Generator的Generate方法,均接受 ChuChart(可将任一 Parser 产出的 ChuChart 互相传入)。
168171
// 同上,alerts是生成过程中可能产生的警告信息等,建议打印出来。
169172
```
170173
174+
#### Ongeki游戏支持
175+
由于这款游戏尚无普遍通用的社区自制谱格式,因此目前MuConvert支持的仅有OGKR(官方游戏格式)这一种格式的解析和生成。
176+
解析得到的谱面表示对象为`OgkChart`类,内含`Notes`(正键、侧键、bell等各种音符)、`Lanes`(轨道和引导线)、`Bullets`(子弹)等字段、足以表达一个谱面的所有信息。具体的含义和用法,`chart/ogk/OgkChart.cs`的代码中有丰富的注释,请参见代码中的注释。
177+
178+
关于其用途,您可以考虑把本项目作为您其他Ongeki相关项目的一个依赖库,通过使用其中的OGKR解析和/或OGKR生成的代码逻辑,来简化/优化您的开发工作,避免关注Ongeki游戏谱面格式的繁杂细节,直接把精力放在您的项目的核心。
179+
例如,如果您正在开发一个Ongeki的谱面播放器,您就可以直接使用`OgkrParser`得到`OgkChart`对象,该对象中内置了关于谱面的所有细节信息;其中的音符类型(`OgkNote`)等上面都实现了丰富的方法,如小节时间`Time`、绝对时间`ToSecond()`等,可以直接调用使用、以便播放器直接计算各个音符在某一时刻的位置,而不必自己去解析OGKR文本、处理各种复杂的谱面相关细节问题了。
180+
181+
以下示例展示了如何解析OGKR文本为OgkChart对象,做一点小小的改动(本例中为把第一个Hold的时长加倍),再生成回OGKR格式的文本。
182+
> 以下 C# 示例中的各类均位于命名空间 `MuConvert.ogk`中,使用时需添加 `using MuConvert.ogk;`
183+
```csharp
184+
// 首先使用File.ReadAllText等方法,将谱面整体读取为字符串
185+
var (ogkChart, alerts) = new OngekiParser().Parse(ogkrText); // 解析 OGKR 谱面字符串
186+
// ogkChart即为OgkChart类的对象,包含了谱面中的所有信息;alerts是解析过程中可能产生的警告信息等,建议打印出来(直接对Alert对象调用ToString()即可)。
187+
188+
// 对ogkChart进行一些你想要的改动,例如把第一个Hold的时长加倍
189+
foreach (OgkNote note in ogkChart.Notes)
190+
{
191+
if (note is Hold hold)
192+
{
193+
var originDuration = hold.EndTime - hold.StartTime; // 计算原始时长
194+
hold.EndTime = originDuration * 2 + hold.StartTime; // 把第一个Hold的时长加倍
195+
break;
196+
}
197+
}
198+
199+
var (ogkrText, alerts) = new OngekiGenerator().Generate(ogkChart); // 将 OgkChart 对象导出为 OGKR 文本
200+
// 同上,alerts是生成过程中可能产生的警告信息等,建议打印出来。
201+
```
202+
171203
#### parser和generator的选项
172204
- 部分parser和generator,在其构造参数中带有可选的选项参数,可以控制转谱时的一些行为。
173205
- SimaiParser带有以下选项:
@@ -219,6 +251,8 @@ finally
219251
220252
- **中间表示 IR(Chart)**:MuConvert 内部统一的谱面数据结构
221253
- 对maimai,类型为 `MuConvert.mai.MaiChart`
254+
- 对CHUNITHM,类型为 `MuConvert.chu.ChuChart`
255+
- 对Ongeki,类型为 `MuConvert.ogk.OgkChart`
222256
- 关键字段包括 `Chart.BpmList``Chart.Notes`,以及 `Touch/Hold/Slide` 等具体 `Note` 子类
223257
224258
- **generator(生成器)**:把中间表示转回“目标格式文本”

chart/ogk/OgkChart.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public class OgkChart: BaseChart<OgkNote>
88
{
99
public string Designer { get; set; } = ""; // 谱师
1010

11-
public decimal ProgJudgeBpm = 240m; // 用于给Hold生成中间判定点的BPM值
11+
public decimal ProgJudgeBpm = 240m; // 用于给Hold生成中间判定点的BPM值,绝大多数情况下不需要手动改动。
1212

1313
// 全局声明的“子弹伤害类型与伤害数值的映射关系”。绝大多数情况下都是这个默认值,不需要做改动。
1414
public Dictionary<BulletDamage, decimal> BulletDamages = new() {

tests/ogk/Ogkr测试.cs

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public class Ogkr测试
1616
public static IEnumerable<object[]> GetTestInputsOnlyLv3(string dataDir) => GetTestInputs(dataDir)
1717
.Where(x=>int.TryParse(((OgkrTestInput)x[0]).DifficultyId, out var id) && id == 3);
1818

19-
[Theory(Skip = "ogkr的解析和生成还没实现完,所以暂时跳过")]
19+
[Theory]
2020
[MemberData(nameof(GetTestInputs), "官谱")]
2121
public void 解析Ogkr再生成回去(OgkrTestInput c)
2222
{
@@ -83,8 +83,11 @@ private void AssertOgkChartOk(OgkChart chart, IEnumerable<Alert> alerts)
8383
/// 1. [HEADER]:忽略 T_ 开头的统计量与 TUTORIAL,其余逐行严格比较。
8484
/// 2. [B_PALETTE]:不直接逐行比较;但要求 actual 内不存在两行“实质相同”
8585
/// (除去 ID 之外其余字段全部相同)。
86-
/// 3. [COMPOSITION] / [LANE] / [LANE_BLOCK] / [BEAM] / [FLICK] / [NOTES]:逐行严格比较。
87-
/// 4. [BULLET] / [BELL]:逐行比较,但其中引用 B_PALETTE ID 的字段,比较的是
86+
/// 3. [COMPOSITION] / [LANE] / [BEAM] / [FLICK] / [NOTES]:逐行严格比较。
87+
/// 4. [LANE_BLOCK]:将每一行视为一个多重集元素进行比较(顺序不敏感)。
88+
/// 这是因为官谱中观察到不同谱面对同一时刻的多条LBK使用了不同的排序约定,
89+
/// 且其在游戏内并无实际顺序意义,故宽松比较以适应这一点。
90+
/// 5. [BULLET] / [BELL]:逐行比较,但其中引用 B_PALETTE ID 的字段,比较的是
8891
/// 所引用的 BPL 行的实质内容(去 ID 后的其余字段)是否相同,而非 ID 字面相等。
8992
///
9093
/// 比较失败时,会打印差异所在 expected 中的行号(1-based)。
@@ -113,7 +116,7 @@ public static void AssertOgkrEqual(string expectedText, string actualText)
113116

114117
CompareSimpleSection("COMPOSITION", expectedSections, actualSections);
115118
CompareSimpleSection("LANE", expectedSections, actualSections);
116-
CompareSimpleSection("LANE_BLOCK", expectedSections, actualSections);
119+
CompareUnorderedSection("LANE_BLOCK", expectedSections, actualSections);
117120
CompareSimpleSection("BEAM", expectedSections, actualSections);
118121
CompareSimpleSection("FLICK", expectedSections, actualSections);
119122
CompareSimpleSection("NOTES", expectedSections, actualSections);
@@ -176,6 +179,35 @@ private static void CompareSimpleSection(string section,
176179
CompareLinesStrict(section, expected, actual);
177180
}
178181

182+
/// <summary>
183+
/// 把两个section内的所有行作为multiset做比较:只关心是否同时存在相同的行集合(含重数),不关心顺序。
184+
/// </summary>
185+
private static void CompareUnorderedSection(string section,
186+
Dictionary<string, List<LineEntry>> expectedSections,
187+
Dictionary<string, List<LineEntry>> actualSections)
188+
{
189+
var expected = expectedSections.TryGetValue(section, out var le) ? le : [];
190+
var actual = actualSections.TryGetValue(section, out var la) ? la : [];
191+
192+
var expectedCounts = expected.GroupBy(e => e.Content).ToDictionary(g => g.Key, g => g.Count());
193+
var actualCounts = actual.GroupBy(e => e.Content).ToDictionary(g => g.Key, g => g.Count());
194+
195+
var diffs = new List<string>();
196+
foreach (var (k, ec) in expectedCounts)
197+
{
198+
actualCounts.TryGetValue(k, out var ac);
199+
if (ac != ec) diffs.Add($" missing/uneven in actual (expected x{ec}, actual x{ac}): {k}");
200+
}
201+
foreach (var (k, ac) in actualCounts)
202+
{
203+
if (!expectedCounts.ContainsKey(k)) diffs.Add($" unexpected in actual (x{ac}): {k}");
204+
}
205+
if (diffs.Count > 0)
206+
{
207+
Assert.Fail($"[{section}] section (unordered) mismatch:{Environment.NewLine}{string.Join(Environment.NewLine, diffs)}");
208+
}
209+
}
210+
179211
private static void CompareLinesStrict(string section, List<LineEntry> expected, List<LineEntry> actual)
180212
{
181213
var max = Math.Max(expected.Count, actual.Count);

0 commit comments

Comments
 (0)